tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from dateutil.tz import tzlocal 41from time import sleep 42 43import re 44import json 45import requests 46import traceback as tb 47from typing import Union 48 49from multiprocessing import cpu_count, Lock 50from multiprocessing.pool import ThreadPool 51import pandas as pd 52 53from mako.template import Template # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 54from Templates import * # Some html-templates used by reporting methods in TKSBrokerAPI module 55from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 56from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 57 58from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator) 59from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 60 61import UniLogger as uLog # Logger for TKSBrokerAPI 62 63 64# --- Common technical parameters: 65 66PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 67uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 68uLogger.level = 10 # debug level by default for TKSBrokerAPI module 69uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 70 71__version__ = "1.6" # The "major.minor" version setup here, but build number define at the build-server only 72 73CPU_COUNT = cpu_count() # host's real CPU count 74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 75 76 77class TinkoffBrokerServer: 78 """ 79 This class implements methods to work with Tinkoff broker server. 80 81 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 82 83 About `token`: https://tinkoff.github.io/investAPI/token/ 84 """ 85 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 86 """ 87 Main class init. 88 89 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 90 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 91 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 92 :param useCache: use default cache file with raw data to use instead of `iList`. 93 True by default. Cache is auto-update if new day has come. 94 If you don't want to use cache and always updates raw data then set `useCache=False`. 95 :param defaultCache: path to default cache file. `dump.json` by default. 96 """ 97 if token is None or not token: 98 try: 99 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 100 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 101 102 except KeyError: 103 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 104 raise Exception("Token required") 105 106 else: 107 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 108 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 109 110 if accountId is None or not accountId: 111 try: 112 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 113 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 114 115 except KeyError: 116 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 117 118 else: 119 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 120 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 121 122 self.version = __version__ # duplicate here used TKSBrokerAPI main version 123 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 124 125 Latest version: https://pypi.org/project/tksbrokerapi/ 126 """ 127 128 self.__lock = Lock() # initialize multiprocessing mutex lock 129 130 self.aliases = TKS_TICKER_ALIASES 131 """Some aliases instead official tickers. 132 133 See also: `TKSEnums.TKS_TICKER_ALIASES` 134 """ 135 136 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 137 138 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 139 140 self._ticker = "" 141 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 142 143 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 144 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 145 146 See also: `SearchByTicker()`, `SearchInstruments()`. 147 """ 148 149 self._figi = "" 150 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 151 152 See also: `SearchByFIGI()`, `SearchInstruments()`. 153 """ 154 155 self.depth = 1 156 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 157 158 See also: `GetCurrentPrices()`. 159 """ 160 161 self.server = r"https://invest-public-api.tinkoff.ru/rest" 162 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 163 164 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 165 """ 166 167 uLogger.debug("Broker API server: {}".format(self.server)) 168 169 self.timeout = 15 170 """Server operations timeout in seconds. Default: `15`. 171 172 See also: `SendAPIRequest()`. 173 """ 174 175 self.headers = { 176 "Content-Type": "application/json", 177 "accept": "application/json", 178 "Authorization": "Bearer {}".format(self.token), 179 "x-app-name": "Tim55667757.TKSBrokerAPI", 180 } 181 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 182 183 See also: `SendAPIRequest()`. 184 """ 185 186 self.body = None 187 """Request body which send to broker server. Default: `None`. 188 189 See also: `SendAPIRequest()`. 190 """ 191 192 self.moreDebug = False 193 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 194 195 self.useHTMLReports = False 196 """ 197 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 198 199 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 200 """ 201 202 self.historyFile = None 203 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 204 205 See also: `History()`. 206 """ 207 208 self.htmlHistoryFile = "index.html" 209 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 210 211 See also: `ShowHistoryChart()`. 212 """ 213 214 self.instrumentsFile = "instruments.md" 215 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 216 217 See also: `ShowInstrumentsInfo()`. 218 """ 219 220 self.searchResultsFile = "search-results.md" 221 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 222 223 See also: `SearchInstruments()`. 224 """ 225 226 self.pricesFile = "prices.md" 227 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 228 229 See also: `GetListOfPrices()`. 230 """ 231 232 self.infoFile = "info.md" 233 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 234 235 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 236 """ 237 238 self.bondsXLSXFile = "ext-bonds.xlsx" 239 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 240 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 241 242 See also: `ExtendBondsData()`. 243 """ 244 245 self.calendarFile = "calendar.md" 246 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 247 248 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 249 250 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 251 """ 252 253 self.overviewFile = "overview.md" 254 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 255 256 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 257 """ 258 259 self.overviewDigestFile = "overview-digest.md" 260 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 261 262 See also: `Overview()` with parameter `details="digest"`. 263 """ 264 265 self.overviewPositionsFile = "overview-positions.md" 266 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 267 268 See also: `Overview()` with parameter `details="positions"`. 269 """ 270 271 self.overviewOrdersFile = "overview-orders.md" 272 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 273 274 See also: `Overview()` with parameter `details="orders"`. 275 """ 276 277 self.overviewAnalyticsFile = "overview-analytics.md" 278 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 279 280 See also: `Overview()` with parameter `details="analytics"`. 281 """ 282 283 self.overviewBondsCalendarFile = "overview-calendar.md" 284 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 285 286 See also: `Overview()` with parameter `details="calendar"`. 287 """ 288 289 self.reportFile = "deals.md" 290 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 291 292 See also: `Deals()`. 293 """ 294 295 self.withdrawalLimitsFile = "limits.md" 296 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 297 298 See also: `OverviewLimits()` and `RequestLimits()`. 299 """ 300 301 self.userInfoFile = "user-info.md" 302 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 303 304 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 305 """ 306 307 self.userAccountsFile = "accounts.md" 308 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 309 310 See also: `OverviewAccounts()`, `RequestAccounts()`. 311 """ 312 313 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 314 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 315 316 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 317 318 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 319 """ 320 321 self.iList = None # init iList for raw instruments data 322 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 323 324 See also: `Listing()`, `DumpInstruments()`. 325 """ 326 327 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 328 if useCache: 329 if os.path.exists(self.iListDumpFile): 330 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 331 curTime = datetime.now(tzutc()) 332 333 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 334 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 335 336 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 337 338 else: 339 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 340 341 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 342 os.path.abspath(self.iListDumpFile), 343 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 344 )) 345 346 else: 347 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 348 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 349 350 else: 351 self.iList = self.Listing() # request new raw instruments data from broker server 352 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 353 354 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 355 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 356 357 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 358 """ 359 360 @property 361 def ticker(self) -> str: 362 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 363 364 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 365 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 366 367 See also: `SearchByTicker()`, `SearchInstruments()`. 368 """ 369 return self._ticker 370 371 @ticker.setter 372 def ticker(self, value): 373 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 374 375 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 376 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 377 378 See also: `SearchByTicker()`, `SearchInstruments()`. 379 """ 380 self._ticker = str(value).upper() # Tickers may be upper case only 381 382 @property 383 def figi(self) -> str: 384 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 385 386 See also: `SearchByFIGI()`, `SearchInstruments()`. 387 """ 388 return self._figi 389 390 @figi.setter 391 def figi(self, value): 392 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 393 394 See also: `SearchByFIGI()`, `SearchInstruments()`. 395 """ 396 self._figi = str(value).upper() # FIGI may be upper case only 397 398 def _ParseJSON(self, rawData="{}") -> dict: 399 """ 400 Parse JSON from response string. 401 402 :param rawData: this is a string with JSON-formatted text. 403 :return: JSON (dictionary), parsed from server response string. 404 """ 405 responseJSON = json.loads(rawData) if rawData else {} 406 407 if self.moreDebug: 408 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 409 410 return responseJSON 411 412 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 413 """ 414 Send GET or POST request to broker server and receive JSON object. 415 416 self.header: must be defining with dictionary of headers. 417 self.body: if define then used as request body. None by default. 418 self.timeout: global request timeout, 15 seconds by default. 419 :param url: url with REST request. 420 :param reqType: send "GET" or "POST" request. "GET" by default. 421 :param retry: how many times retry after first request if an 5xx server errors occurred. 422 :param pause: sleep time in seconds between retries. 423 :return: response JSON (dictionary) from broker. 424 """ 425 if reqType.upper() not in ("GET", "POST"): 426 uLogger.error("You can define request type: `GET` or `POST`!") 427 raise Exception("Incorrect value") 428 429 if self.moreDebug: 430 uLogger.debug("Request parameters:") 431 uLogger.debug(" - REST API URL: {}".format(url)) 432 uLogger.debug(" - request type: {}".format(reqType)) 433 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 434 uLogger.debug(" - body:\n{}".format(self.body)) 435 436 # fast hack to avoid all operations with some tickers/FIGI 437 responseJSON = {} 438 oK = True 439 for item in self.exclude: 440 if item in url: 441 if self.moreDebug: 442 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 443 444 oK = False 445 break 446 447 if oK: 448 with self.__lock: # acquire the mutex lock 449 counter = 0 450 response = None 451 errMsg = "" 452 453 while not response and counter <= retry: 454 if reqType == "GET": 455 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 456 457 if reqType == "POST": 458 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 459 460 if self.moreDebug: 461 uLogger.debug("Response:") 462 uLogger.debug(" - status code: {}".format(response.status_code)) 463 uLogger.debug(" - reason: {}".format(response.reason)) 464 uLogger.debug(" - body length: {}".format(len(response.text))) 465 uLogger.debug(" - headers:\n{}".format(response.headers)) 466 467 # Server returns some headers: 468 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 469 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 470 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 471 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 472 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 473 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 474 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 475 sleep(rateLimitWait) 476 477 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 478 if 400 <= response.status_code < 500: 479 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 480 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 481 482 if "code" in response.text and "message" in response.text: 483 msgDict = self._ParseJSON(rawData=response.text) 484 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 485 486 counter = retry + 1 # do not retry for 4xx errors 487 488 if 500 <= response.status_code < 600: 489 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 490 uLogger.debug(" - not oK, {}".format(errMsg)) 491 492 if "code" in response.text and "message" in response.text: 493 errMsgDict = self._ParseJSON(rawData=response.text) 494 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 495 496 counter += 1 497 498 if counter <= retry: 499 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 500 sleep(pause) 501 502 responseJSON = self._ParseJSON(rawData=response.text) 503 504 if errMsg: 505 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 506 uLogger.error(" - not oK, {}".format(errMsg)) 507 508 return responseJSON 509 510 def _IUpdater(self, iType: str) -> tuple: 511 """ 512 Request instrument by type from server. See available API methods for instruments: 513 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 514 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 515 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 516 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 517 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 518 519 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 520 :return: tuple with iType name and list of available instruments of current type for defined user token. 521 """ 522 result = [] 523 524 if iType in TKS_INSTRUMENTS: 525 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 526 527 # all instruments have the same body in API v2 requests: 528 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 529 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 530 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 531 532 return iType, result 533 534 def _IWrapper(self, kwargs): 535 """ 536 Wrapper runs instrument's update method `_IUpdater()`. 537 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 538 """ 539 return self._IUpdater(**kwargs) 540 541 def Listing(self) -> dict: 542 """ 543 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 544 545 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 546 """ 547 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 548 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 549 550 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 551 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 552 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 553 554 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 555 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 556 poolUpdater.close() # close the thread pool 557 poolUpdater.join() # wait a moment until all data returns from threads 558 559 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 560 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 561 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 562 563 # calculate minimum price increment (step) for all instruments and set up instrument's type: 564 for iType in iList.keys(): 565 for ticker in iList[iType]: 566 iList[iType][ticker]["type"] = iType 567 568 if "minPriceIncrement" in iList[iType][ticker].keys(): 569 iList[iType][ticker]["step"] = NanoToFloat( 570 iList[iType][ticker]["minPriceIncrement"]["units"], 571 iList[iType][ticker]["minPriceIncrement"]["nano"], 572 ) 573 574 else: 575 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 576 577 return iList 578 579 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 580 """ 581 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 582 583 See also: `DumpInstruments()`, `Listing()`. 584 585 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 586 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 587 """ 588 if self.iListDumpFile is None or not self.iListDumpFile: 589 uLogger.error("Output name of dump file must be defined!") 590 raise Exception("Filename required") 591 592 if not self.iList or forceUpdate: 593 self.iList = self.Listing() 594 595 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 596 597 # Save as XLSX with separated sheets for every type of instruments: 598 with pd.ExcelWriter( 599 path=xlsxDumpFile, 600 date_format=TKS_DATE_FORMAT, 601 datetime_format=TKS_DATE_TIME_FORMAT, 602 mode="w", 603 ) as writer: 604 for iType in TKS_INSTRUMENTS: 605 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 606 df = df[sorted(df)] # sorted by column names 607 df = df.applymap( 608 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 609 na_action="ignore", 610 ) # converting numbers from nano-type to float in every cell 611 df.to_excel( 612 writer, 613 sheet_name=iType, 614 encoding="UTF-8", 615 freeze_panes=(1, 1), 616 ) # saving as XLSX-file with freeze first row and column as headers 617 618 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 619 620 def DumpInstruments(self, forceUpdate: bool = True) -> str: 621 """ 622 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 623 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 624 625 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 626 627 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 628 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 629 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 630 """ 631 if self.iListDumpFile is None or not self.iListDumpFile: 632 uLogger.error("Output name of dump file must be defined!") 633 raise Exception("Filename required") 634 635 if not self.iList or forceUpdate: 636 self.iList = self.Listing() 637 638 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 639 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 640 fH.write(jsonDump) 641 642 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 643 644 return jsonDump 645 646 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 647 """ 648 Show information about one instrument defined by json data and prints it in Markdown format. 649 650 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 651 652 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 653 :param show: if `True` then also printing information about instrument and its current price. 654 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 655 :return: multilines text in Markdown format with information about one instrument. 656 """ 657 splitLine = "| | |\n" 658 infoText = "" 659 660 if iJSON is not None and iJSON and isinstance(iJSON, dict): 661 info = [ 662 "# Main information\n\n", 663 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 664 "| Parameters | Values |\n", 665 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 666 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 667 "| Full name: | {:<54} |\n".format(iJSON["name"]), 668 ] 669 670 if "sector" in iJSON.keys() and iJSON["sector"]: 671 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 672 673 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 674 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 675 676 info.extend([ 677 splitLine, 678 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 679 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 680 ]) 681 682 if "isin" in iJSON.keys() and iJSON["isin"]: 683 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 684 685 if "classCode" in iJSON.keys(): 686 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 687 688 info.extend([ 689 splitLine, 690 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 691 splitLine, 692 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 693 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 694 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 695 ]) 696 697 if iJSON["figi"]: 698 self._figi = iJSON["figi"] 699 iJSON = iJSON | self.RequestTradingStatus() 700 701 info.extend([ 702 splitLine, 703 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 704 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 705 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 706 ]) 707 708 info.append(splitLine) 709 710 if "type" in iJSON.keys() and iJSON["type"]: 711 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 712 713 if "shareType" in iJSON.keys() and iJSON["shareType"]: 714 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 715 716 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 717 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 718 719 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 720 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 721 722 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 723 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 724 725 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 726 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 727 728 if "focusType" in iJSON.keys() and iJSON["focusType"]: 729 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 730 731 if "assetType" in iJSON.keys() and iJSON["assetType"]: 732 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 733 734 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 735 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 736 737 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 738 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 739 740 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 741 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 742 743 if "currency" in iJSON.keys(): 744 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 745 746 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 747 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 748 749 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 750 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 751 752 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 753 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 754 755 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 756 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 757 758 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 759 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 760 761 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 762 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 763 764 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 765 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 766 767 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 768 info.append("| Perpetual bond: | Yes |\n") 769 770 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 771 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 772 773 iExt = None 774 if iJSON["type"] == "Bonds": 775 info.extend([ 776 splitLine, 777 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 778 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 779 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 780 iJSON["nominal"]["currency"], 781 )), 782 ]) 783 784 if "floatingCouponFlag" in iJSON.keys(): 785 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 786 787 if "amortizationFlag" in iJSON.keys(): 788 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 789 790 info.append(splitLine) 791 792 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 793 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 794 795 if iJSON["figi"]: 796 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 797 798 info.extend([ 799 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 800 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 801 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 802 ]) 803 804 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 805 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 806 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 807 iJSON["aciValue"]["currency"] 808 ))) 809 810 if "currentPrice" in iJSON.keys(): 811 info.append(splitLine) 812 813 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 814 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 815 816 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 817 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 818 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 819 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 820 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 821 822 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 823 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 824 825 info.extend([ 826 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 827 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 828 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 829 )), 830 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 831 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 832 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 833 )), 834 "| Changes between last deal price and last close | {:<54} |\n".format( 835 "{:.2f}%{}".format( 836 iJSON["currentPrice"]["changes"], 837 " ({}{:.2f} {})".format( 838 "+" if bondChangesDelta > 0 else "", 839 bondChangesDelta, 840 aciCurrency 841 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 842 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 843 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 844 currency 845 ), 846 ) 847 ), 848 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 849 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 850 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 851 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 852 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 853 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 854 )), 855 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 856 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 857 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 858 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 859 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 860 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 861 )), 862 ]) 863 864 if "lot" in iJSON.keys(): 865 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 866 867 if "step" in iJSON.keys() and iJSON["step"] != 0: 868 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 869 870 # Add bond payment calendar: 871 if iJSON["type"] == "Bonds": 872 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 873 info.extend(["\n#", strCalendar]) 874 875 infoText += "".join(info) 876 877 if show and not onlyFiles: 878 uLogger.info("{}".format(infoText)) 879 880 if self.infoFile is not None and (show or onlyFiles): 881 with open(self.infoFile, "w", encoding="UTF-8") as fH: 882 fH.write(infoText) 883 884 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 885 886 if self.useHTMLReports: 887 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 888 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 889 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 890 891 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 892 893 return infoText 894 895 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 896 """ 897 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 898 899 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 900 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 901 :return: JSON formatted data with information about instrument. 902 """ 903 tickerJSON = {} 904 if self.moreDebug: 905 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 906 907 if not self._ticker: 908 uLogger.warning("self._ticker variable is not be empty!") 909 910 else: 911 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 912 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 913 raise Exception("Instrument not allowed") 914 915 if not self.iList: 916 self.iList = self.Listing() 917 918 if self._ticker in self.iList["Shares"].keys(): 919 tickerJSON = self.iList["Shares"][self._ticker] 920 if self.moreDebug: 921 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 922 923 elif self._ticker in self.iList["Currencies"].keys(): 924 tickerJSON = self.iList["Currencies"][self._ticker] 925 if self.moreDebug: 926 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 927 928 elif self._ticker in self.iList["Bonds"].keys(): 929 tickerJSON = self.iList["Bonds"][self._ticker] 930 if self.moreDebug: 931 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 932 933 elif self._ticker in self.iList["Etfs"].keys(): 934 tickerJSON = self.iList["Etfs"][self._ticker] 935 if self.moreDebug: 936 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 937 938 elif self._ticker in self.iList["Futures"].keys(): 939 tickerJSON = self.iList["Futures"][self._ticker] 940 if self.moreDebug: 941 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 942 943 if tickerJSON: 944 self._figi = tickerJSON["figi"] 945 946 if requestPrice: 947 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 948 949 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 950 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 951 952 else: 953 tickerJSON["currentPrice"]["changes"] = 0 954 955 if show: 956 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 957 958 else: 959 if show: 960 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 961 962 return tickerJSON 963 964 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 965 """ 966 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 967 968 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 969 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 970 :return: JSON formatted data with information about instrument. 971 """ 972 figiJSON = {} 973 if self.moreDebug: 974 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 975 976 if not self._figi: 977 uLogger.warning("self._figi variable is not be empty!") 978 979 else: 980 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 981 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 982 raise Exception("Instrument not allowed") 983 984 if not self.iList: 985 self.iList = self.Listing() 986 987 for item in self.iList["Shares"].keys(): 988 if self._figi == self.iList["Shares"][item]["figi"]: 989 figiJSON = self.iList["Shares"][item] 990 991 if self.moreDebug: 992 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 993 994 break 995 996 if not figiJSON: 997 for item in self.iList["Currencies"].keys(): 998 if self._figi == self.iList["Currencies"][item]["figi"]: 999 figiJSON = self.iList["Currencies"][item] 1000 1001 if self.moreDebug: 1002 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1003 1004 break 1005 1006 if not figiJSON: 1007 for item in self.iList["Bonds"].keys(): 1008 if self._figi == self.iList["Bonds"][item]["figi"]: 1009 figiJSON = self.iList["Bonds"][item] 1010 1011 if self.moreDebug: 1012 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1013 1014 break 1015 1016 if not figiJSON: 1017 for item in self.iList["Etfs"].keys(): 1018 if self._figi == self.iList["Etfs"][item]["figi"]: 1019 figiJSON = self.iList["Etfs"][item] 1020 1021 if self.moreDebug: 1022 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1023 1024 break 1025 1026 if not figiJSON: 1027 for item in self.iList["Futures"].keys(): 1028 if self._figi == self.iList["Futures"][item]["figi"]: 1029 figiJSON = self.iList["Futures"][item] 1030 1031 if self.moreDebug: 1032 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1033 1034 break 1035 1036 if figiJSON: 1037 self._figi = figiJSON["figi"] 1038 self._ticker = figiJSON["ticker"] 1039 1040 if requestPrice: 1041 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1042 1043 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1044 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1045 1046 else: 1047 figiJSON["currentPrice"]["changes"] = 0 1048 1049 if show: 1050 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1051 1052 else: 1053 if show: 1054 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1055 1056 return figiJSON 1057 1058 def GetCurrentPrices(self, show: bool = True) -> dict: 1059 """ 1060 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1061 `{"buy": [{"price": 1243.8, "quantity": 193}, 1062 {"price": 1244.0, "quantity": 168}, 1063 {"price": 1244.8, "quantity": 5}, 1064 {"price": 1245.0, "quantity": 61}, 1065 {"price": 1245.4, "quantity": 60}], 1066 "sell": [{"price": 1243.6, "quantity": 8}, 1067 {"price": 1242.6, "quantity": 10}, 1068 {"price": 1242.4, "quantity": 18}, 1069 {"price": 1242.2, "quantity": 50}, 1070 {"price": 1242.0, "quantity": 113}], 1071 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1072 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1073 - sell: list of dicts with Buyers prices, 1074 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1075 - quantity: volume value by current price in lots, 1076 - limitUp: current trade session limit price, maximum, 1077 - limitDown: current trade session limit price, minimum, 1078 - lastPrice: last deal price of the instrument, 1079 - closePrice: previous trade session close price of the instrument. 1080 1081 See also: `SearchByTicker()` and `SearchByFIGI()`. 1082 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1083 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1084 1085 :param show: if `True` then print DOM to log and console. 1086 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1087 If an error occurred then returns an empty record: 1088 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1089 """ 1090 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1091 1092 if self.depth < 1: 1093 uLogger.error("Depth of Market (DOM) must be >=1!") 1094 raise Exception("Incorrect value") 1095 1096 if not (self._ticker or self._figi): 1097 uLogger.error("self._ticker or self._figi variables must be defined!") 1098 raise Exception("Ticker or FIGI required") 1099 1100 if self._ticker and not self._figi: 1101 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1102 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1103 1104 if not self._ticker and self._figi: 1105 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1106 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1107 1108 if not self._figi: 1109 uLogger.error("FIGI is not defined!") 1110 raise Exception("Ticker or FIGI required") 1111 1112 else: 1113 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1114 1115 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1116 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1117 self.body = str({"figi": self._figi, "depth": self.depth}) 1118 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1119 1120 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1121 # list of dicts with sellers orders: 1122 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1123 1124 # list of dicts with buyers orders: 1125 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1126 1127 # max price of instrument at this time: 1128 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1129 1130 # min price of instrument at this time: 1131 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1132 1133 # last price of deal with instrument: 1134 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1135 1136 # last close price of instrument: 1137 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1138 1139 else: 1140 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1141 uLogger.debug("Server response: {}".format(pricesResponse)) 1142 1143 if show: 1144 if prices["buy"] or prices["sell"]: 1145 info = [ 1146 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1147 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1148 self._ticker, 1149 self._figi, 1150 self.depth, 1151 ), 1152 "-" * 60, "\n", 1153 " Orders of Buyers | Orders of Sellers\n", 1154 "-" * 60, "\n", 1155 " Sell prices (volumes) | Buy prices (volumes)\n", 1156 "-" * 60, "\n", 1157 ] 1158 1159 if not prices["buy"]: 1160 info.append(" | No orders!\n") 1161 sumBuy = 0 1162 1163 else: 1164 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1165 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1166 for item in maxMinSorted: 1167 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1168 1169 if not prices["sell"]: 1170 info.append("No orders! |\n") 1171 sumSell = 0 1172 1173 else: 1174 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1175 for item in prices["sell"]: 1176 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1177 1178 info.extend([ 1179 "-" * 60, "\n", 1180 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1181 "-" * 60, "\n", 1182 ]) 1183 1184 infoText = "".join(info) 1185 1186 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1187 1188 else: 1189 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1190 1191 return prices 1192 1193 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1194 """ 1195 This method get and show information about all available broker instruments for current user account. 1196 If `instrumentsFile` string is not empty then also save information to this file. 1197 1198 :param show: if `True` then print results to console, if `False` — print only to file. 1199 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1200 :return: multi-lines string with all available broker instruments. 1201 """ 1202 if not self.iList: 1203 self.iList = self.Listing() 1204 1205 info = [ 1206 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1207 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1208 ] 1209 1210 # add instruments count by type: 1211 for iType in self.iList.keys(): 1212 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1213 1214 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1215 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1216 1217 # generating info tables with all instruments by type: 1218 for iType in self.iList.keys(): 1219 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1220 1221 for instrument in self.iList[iType].keys(): 1222 iName = self.iList[iType][instrument]["name"] # instrument's name 1223 if len(iName) > 57: 1224 iName = "{}...".format(iName[:54]) # right trim for a long string 1225 1226 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1227 self.iList[iType][instrument]["ticker"], 1228 iName, 1229 self.iList[iType][instrument]["figi"], 1230 self.iList[iType][instrument]["currency"], 1231 self.iList[iType][instrument]["lot"], 1232 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1233 )) 1234 1235 infoText = "".join(info) 1236 1237 if show and not onlyFiles: 1238 uLogger.info(infoText) 1239 1240 if self.instrumentsFile and (show or onlyFiles): 1241 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1242 fH.write(infoText) 1243 1244 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1245 1246 if self.useHTMLReports: 1247 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1248 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1249 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1250 1251 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1252 1253 return infoText 1254 1255 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1256 """ 1257 This method search and show information about instruments by part of its ticker, FIGI or name. 1258 If `searchResultsFile` string is not empty then also save information to this file. 1259 1260 :param pattern: string with part of ticker, FIGI or instrument's name. 1261 :param show: if `True` then print results to console, if `False` — return list of result only. 1262 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1263 :return: list of dictionaries with all found instruments. 1264 """ 1265 if not self.iList: 1266 self.iList = self.Listing() 1267 1268 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1269 compiledPattern = re.compile(pattern, re.IGNORECASE) 1270 1271 for iType in self.iList: 1272 for instrument in self.iList[iType].values(): 1273 searchResult = compiledPattern.search(" ".join( 1274 [instrument["ticker"], instrument["figi"], instrument["name"]] 1275 )) 1276 1277 if searchResult: 1278 searchResults[iType][instrument["ticker"]] = instrument 1279 1280 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1281 info = [ 1282 "# Search results\n\n", 1283 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1284 "* **Search pattern:** [{}]\n".format(pattern), 1285 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1286 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1287 ] 1288 infoShort = info[:] 1289 1290 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1291 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1292 skippedLine = "| ... | ... | ... | ... |\n" 1293 1294 if resultsLen == 0: 1295 info.append("\nNo results\n") 1296 infoShort.append("\nNo results\n") 1297 uLogger.warning("No results. Try changing your search pattern.") 1298 1299 else: 1300 for iType in searchResults: 1301 iTypeValuesCount = len(searchResults[iType].values()) 1302 if iTypeValuesCount > 0: 1303 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1304 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1305 1306 for instrument in searchResults[iType].values(): 1307 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1308 instrument["type"], 1309 instrument["ticker"], 1310 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1311 instrument["figi"], 1312 )) 1313 1314 if iTypeValuesCount <= 5: 1315 infoShort.extend(info[-iTypeValuesCount:]) 1316 1317 else: 1318 infoShort.extend(info[-5:]) 1319 infoShort.append(skippedLine) 1320 1321 infoText = "".join(info) 1322 infoTextShort = "".join(infoShort) 1323 1324 if show and not onlyFiles: 1325 uLogger.info(infoTextShort) 1326 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1327 1328 if self.searchResultsFile and (show or onlyFiles): 1329 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1330 fH.write(infoText) 1331 1332 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1333 1334 if self.useHTMLReports: 1335 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1336 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1337 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1338 1339 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1340 1341 return searchResults 1342 1343 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1344 """ 1345 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1346 1347 :param instruments: list of strings with tickers or FIGIs. 1348 :return: list with unique instrument FIGIs only. 1349 """ 1350 requestedInstruments = [] 1351 for iName in instruments: 1352 if iName not in self.aliases.keys(): 1353 if iName not in requestedInstruments: 1354 requestedInstruments.append(iName) 1355 1356 else: 1357 if iName not in requestedInstruments: 1358 if self.aliases[iName] not in requestedInstruments: 1359 requestedInstruments.append(self.aliases[iName]) 1360 1361 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1362 1363 onlyUniqueFIGIs = [] 1364 for iName in requestedInstruments: 1365 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1366 continue 1367 1368 self._ticker = iName 1369 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1370 1371 if not iData: 1372 self._ticker = "" 1373 self._figi = iName 1374 1375 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1376 1377 if not iData: 1378 self._figi = "" 1379 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1380 1381 if iData and iData["figi"] not in onlyUniqueFIGIs: 1382 onlyUniqueFIGIs.append(iData["figi"]) 1383 1384 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1385 1386 return onlyUniqueFIGIs 1387 1388 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1389 """ 1390 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1391 1392 See limits: https://tinkoff.github.io/investAPI/limits/ 1393 1394 If `pricesFile` string is not empty then also save information to this file. 1395 1396 :param instruments: list of strings with tickers or FIGIs. 1397 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1398 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1399 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1400 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1401 """ 1402 if instruments is None or not instruments: 1403 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1404 raise Exception("Ticker or FIGI required") 1405 1406 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1407 1408 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1409 1410 iList = [] # trying to get info and current prices about all unique instruments: 1411 for self._figi in onlyUniqueFIGIs: 1412 iData = self.SearchByFIGI(requestPrice=True, show=False) 1413 iList.append(iData) 1414 1415 self.ShowListOfPrices(iList, show, onlyFiles) 1416 1417 return iList 1418 1419 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1420 """ 1421 Show table contains current prices of given instruments. 1422 1423 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1424 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1425 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1426 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1427 :return: multilines text in Markdown format as a table contains current prices. 1428 """ 1429 infoText = "" 1430 1431 if show or self.pricesFile or onlyFiles: 1432 info = [ 1433 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1434 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1435 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1436 ] 1437 1438 for item in iList: 1439 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1440 item["ticker"], 1441 item["figi"], 1442 item["type"], 1443 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1444 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1445 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1446 "{} / {}".format( 1447 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1448 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1449 ), 1450 "{} / {}".format( 1451 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1452 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1453 ), 1454 item["currency"], 1455 )) 1456 1457 infoText = "".join(info) 1458 1459 if show and not onlyFiles: 1460 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1461 1462 if self.pricesFile and (show or onlyFiles): 1463 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1464 fH.write(infoText) 1465 1466 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1467 1468 if self.useHTMLReports: 1469 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1470 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1471 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1472 1473 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1474 1475 return infoText 1476 1477 def RequestTradingStatus(self) -> dict: 1478 """ 1479 Requesting trading status for the instrument defined by `figi` variable. 1480 1481 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1482 1483 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1484 1485 :return: dictionary with trading status attributes. Response example: 1486 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1487 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1488 """ 1489 if self._figi is None or not self._figi: 1490 uLogger.error("Variable `figi` must be defined for using this method!") 1491 raise Exception("FIGI required") 1492 1493 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1494 1495 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1496 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1497 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1498 1499 if self.moreDebug: 1500 uLogger.debug("Records about current trading status successfully received") 1501 1502 return tradingStatus 1503 1504 def RequestPortfolio(self) -> dict: 1505 """ 1506 Requesting actual user's portfolio for current `accountId`. 1507 1508 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1509 1510 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1511 1512 :return: dictionary with user's portfolio. 1513 """ 1514 if self.accountId is None or not self.accountId: 1515 uLogger.error("Variable `accountId` must be defined for using this method!") 1516 raise Exception("Account ID required") 1517 1518 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1519 1520 self.body = str({"accountId": self.accountId}) 1521 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1522 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1523 1524 if self.moreDebug: 1525 uLogger.debug("Records about user's portfolio successfully received") 1526 1527 return rawPortfolio 1528 1529 def RequestPositions(self) -> dict: 1530 """ 1531 Requesting open positions by currencies and instruments for current `accountId`. 1532 1533 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1534 1535 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1536 1537 :return: dictionary with open positions by instruments. 1538 """ 1539 if self.accountId is None or not self.accountId: 1540 uLogger.error("Variable `accountId` must be defined for using this method!") 1541 raise Exception("Account ID required") 1542 1543 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1544 1545 self.body = str({"accountId": self.accountId}) 1546 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1547 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1548 1549 if self.moreDebug: 1550 uLogger.debug("Records about current open positions successfully received") 1551 1552 return rawPositions 1553 1554 def RequestPendingOrders(self) -> list: 1555 """ 1556 Requesting current actual pending limit orders for current `accountId`. 1557 1558 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1559 1560 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1561 1562 :return: list of dictionaries with pending limit orders. 1563 """ 1564 if self.accountId is None or not self.accountId: 1565 uLogger.error("Variable `accountId` must be defined for using this method!") 1566 raise Exception("Account ID required") 1567 1568 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1569 1570 self.body = str({"accountId": self.accountId}) 1571 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1572 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1573 1574 if "orders" in rawResponse.keys(): 1575 rawOrders = rawResponse["orders"] 1576 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1577 1578 else: 1579 rawOrders = [] 1580 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1581 1582 return rawOrders 1583 1584 def RequestStopOrders(self) -> list: 1585 """ 1586 Requesting current actual stop orders for current `accountId`. 1587 1588 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1589 1590 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1591 1592 :return: list of dictionaries with stop orders. 1593 """ 1594 if self.accountId is None or not self.accountId: 1595 uLogger.error("Variable `accountId` must be defined for using this method!") 1596 raise Exception("Account ID required") 1597 1598 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1599 1600 self.body = str({"accountId": self.accountId}) 1601 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1602 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1603 1604 if "stopOrders" in rawResponse.keys(): 1605 rawStopOrders = rawResponse["stopOrders"] 1606 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1607 1608 else: 1609 rawStopOrders = [] 1610 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1611 1612 return rawStopOrders 1613 1614 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1615 """ 1616 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1617 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1618 and `overviewBondsCalendarFile` are defined then also save information to file. 1619 1620 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1621 many requests about the state of the portfolio, and then, based on the received data, a large number 1622 of calculation and statistics are collected. 1623 1624 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1625 :param details: how detailed should the information be? 1626 - `full` — shows full available information about portfolio status (by default), 1627 - `positions` — shows only open positions, 1628 - `orders` — shows only sections of open limits and stop orders. 1629 - `digest` — show a short digest of the portfolio status, 1630 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1631 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1632 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1633 :return: dictionary with client's raw portfolio and some statistics. 1634 """ 1635 if self.accountId is None or not self.accountId: 1636 uLogger.error("Variable `accountId` must be defined for using this method!") 1637 raise Exception("Account ID required") 1638 1639 view = { 1640 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1641 "headers": {}, # list of dictionaries, response headers without "positions" section 1642 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1643 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1644 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1645 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1646 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1647 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1648 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1649 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1650 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1651 }, 1652 "stat": { # --- some statistics calculated using "raw" sections: 1653 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1654 "availableRUB": 0., # available rubles (without other currencies) 1655 "blockedRUB": 0., # blocked sum in Russian Rouble 1656 "totalChangesRUB": 0., # changes for all open trades in RUB 1657 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1658 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1659 "sharesCostRUB": 0., # costs of all shares in RUB 1660 "bondsCostRUB": 0., # costs of all bonds in RUB 1661 "etfsCostRUB": 0., # costs of all etfs in RUB 1662 "futuresCostRUB": 0., # costs of all futures in RUB 1663 "Currencies": [], # list of dictionaries of all currencies statistics 1664 "Shares": [], # list of dictionaries of all shares statistics 1665 "Bonds": [], # list of dictionaries of all bonds statistics 1666 "Etfs": [], # list of dictionaries of all etfs statistics 1667 "Futures": [], # list of dictionaries of all futures statistics 1668 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1669 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1670 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1671 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1672 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1673 }, 1674 "analytics": { # --- some analytics of portfolio: 1675 "distrByAssets": {}, # portfolio distribution by assets 1676 "distrByCompanies": {}, # portfolio distribution by companies 1677 "distrBySectors": {}, # portfolio distribution by sectors 1678 "distrByCurrencies": {}, # portfolio distribution by currencies 1679 "distrByCountries": {}, # portfolio distribution by countries 1680 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1681 } 1682 } 1683 1684 details = details.lower() 1685 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1686 if details not in availableDetails: 1687 details = "full" 1688 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1689 1690 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1691 1692 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1693 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1694 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1695 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1696 1697 # save response headers without "positions" section: 1698 for key in portfolioResponse.keys(): 1699 if key != "positions": 1700 view["raw"]["headers"][key] = portfolioResponse[key] 1701 1702 else: 1703 continue 1704 1705 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1706 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1707 for item in portfolioResponse["positions"]: 1708 if item["instrumentType"] == "currency": 1709 self._figi = item["figi"] 1710 if not self._figi and item["ticker"]: 1711 self._ticker = item["ticker"] 1712 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1713 1714 curr = self.SearchByFIGI(requestPrice=False) 1715 1716 # current price of currency in RUB: 1717 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1718 "name": curr["name"], 1719 "currentPrice": NanoToFloat( 1720 item["currentPrice"]["units"], 1721 item["currentPrice"]["nano"] 1722 ), 1723 } 1724 1725 view["raw"]["Currencies"].append(item) 1726 1727 elif item["instrumentType"] == "share": 1728 view["raw"]["Shares"].append(item) 1729 1730 elif item["instrumentType"] == "bond": 1731 view["raw"]["Bonds"].append(item) 1732 1733 elif item["instrumentType"] == "etf": 1734 view["raw"]["Etfs"].append(item) 1735 1736 elif item["instrumentType"] == "futures": 1737 view["raw"]["Futures"].append(item) 1738 1739 else: 1740 continue 1741 1742 # how many volume of currencies (by ISO currency name) are blocked: 1743 for item in view["raw"]["positions"]["blocked"]: 1744 blocked = NanoToFloat(item["units"], item["nano"]) 1745 if blocked > 0: 1746 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1747 1748 # how many volume of instruments (by FIGI) are blocked: 1749 for item in view["raw"]["positions"]["securities"]: 1750 blocked = int(item["blocked"]) 1751 if blocked > 0: 1752 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1753 1754 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1755 1756 if "rub" in allBlocked.keys(): 1757 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1758 1759 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1760 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1761 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1762 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1763 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1764 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1765 view["stat"]["portfolioCostRUB"] = sum([ 1766 view["stat"]["allCurrenciesCostRUB"], 1767 view["stat"]["sharesCostRUB"], 1768 view["stat"]["bondsCostRUB"], 1769 view["stat"]["etfsCostRUB"], 1770 view["stat"]["futuresCostRUB"], 1771 ]) 1772 1773 # --- calculating some portfolio statistics: 1774 byComp = {} # distribution by companies 1775 bySect = {} # distribution by sectors 1776 byCurr = {} # distribution by currencies (include RUB) 1777 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1778 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1779 1780 for item in portfolioResponse["positions"]: 1781 self._figi = item["figi"] 1782 if not self._figi and item["ticker"]: 1783 self._ticker = item["ticker"] 1784 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1785 1786 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1787 1788 if instrument: 1789 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1790 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1791 1792 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1793 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1794 1795 else: 1796 blocked = 0 1797 1798 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1799 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1800 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1801 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1802 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1803 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1804 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1805 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1806 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1807 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1808 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1809 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1810 1811 statData = { 1812 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1813 "ticker": instrument["ticker"], # ticker by FIGI 1814 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1815 "volume": volume, # available volume of instrument 1816 "lots": lots, # volume in lots of instrument 1817 "direction": direction, # direction of an instrument's position: short or long 1818 "blocked": blocked, # blocked volume of currency or instrument 1819 "currentPrice": curPrice, # current instrument's price in basic asset 1820 "average": average, # current average position price 1821 "cost": cost, # current cost of all volume of instrument in basic asset 1822 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1823 "costRUB": costRUB, # cost of instrument in ruble 1824 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1825 "profit": profit, # expected profit at current moment 1826 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1827 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1828 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1829 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1830 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1831 "step": instrument["step"], # minimum price increment 1832 } 1833 1834 # adding distribution by unique countries: 1835 if statData["country"] not in byCountry.keys(): 1836 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1837 1838 else: 1839 byCountry[statData["country"]]["cost"] += costRUB 1840 byCountry[statData["country"]]["percent"] += percentCostRUB 1841 1842 if item["instrumentType"] != "currency": 1843 # adding distribution by unique companies: 1844 if statData["name"]: 1845 if statData["name"] not in byComp.keys(): 1846 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1847 1848 else: 1849 byComp[statData["name"]]["cost"] += costRUB 1850 byComp[statData["name"]]["percent"] += percentCostRUB 1851 1852 # adding distribution by unique sectors: 1853 if statData["sector"] not in bySect.keys(): 1854 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1855 1856 else: 1857 bySect[statData["sector"]]["cost"] += costRUB 1858 bySect[statData["sector"]]["percent"] += percentCostRUB 1859 1860 # adding distribution by unique currencies: 1861 if currency not in byCurr.keys(): 1862 byCurr[currency] = { 1863 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1864 "cost": costRUB, 1865 "percent": percentCostRUB 1866 } 1867 1868 else: 1869 byCurr[currency]["cost"] += costRUB 1870 byCurr[currency]["percent"] += percentCostRUB 1871 1872 # saving statistics for every instrument: 1873 if item["instrumentType"] == "currency": 1874 view["stat"]["Currencies"].append(statData) 1875 1876 # update dict with free funds for trading (total - blocked) by currencies 1877 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1878 view["stat"]["funds"][currency] = { 1879 "total": volume, 1880 "totalCostRUB": costRUB, # total volume cost in rubles 1881 "free": volume - blocked, 1882 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1883 } 1884 1885 elif item["instrumentType"] == "share": 1886 view["stat"]["Shares"].append(statData) 1887 1888 elif item["instrumentType"] == "bond": 1889 view["stat"]["Bonds"].append(statData) 1890 1891 elif item["instrumentType"] == "etf": 1892 view["stat"]["Etfs"].append(statData) 1893 1894 elif item["instrumentType"] == "Futures": 1895 view["stat"]["Futures"].append(statData) 1896 1897 else: 1898 continue 1899 1900 # total changes in Russian Ruble: 1901 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1902 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1903 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1904 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1905 view["stat"]["funds"]["rub"] = { 1906 "total": view["stat"]["availableRUB"], 1907 "totalCostRUB": view["stat"]["availableRUB"], 1908 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1909 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1910 } 1911 1912 # --- pending limit orders sector data: 1913 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1914 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1915 1916 for item in view["raw"]["orders"]: 1917 self._figi = item["figi"] 1918 1919 if item["figi"] not in uniquePendingOrdersFIGIs: 1920 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1921 1922 uniquePendingOrdersFIGIs.append(item["figi"]) 1923 uniquePendingOrders[item["figi"]] = instrument 1924 1925 else: 1926 instrument = uniquePendingOrders[item["figi"]] 1927 1928 if instrument: 1929 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1930 orderType = TKS_ORDER_TYPES[item["orderType"]] 1931 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1932 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1933 1934 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1935 if item["direction"] == "ORDER_DIRECTION_BUY": 1936 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1937 1938 else: 1939 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1940 1941 # requested price for order execution: 1942 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1943 1944 # necessary changes in percent to reach target from current price: 1945 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1946 1947 view["stat"]["orders"].append({ 1948 "orderID": item["orderId"], # orderId number parameter of current order 1949 "figi": item["figi"], # FIGI identification 1950 "ticker": instrument["ticker"], # ticker name by FIGI 1951 "lotsRequested": item["lotsRequested"], # requested lots value 1952 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1953 "currentPrice": lastPrice, # current instrument's price for defined action 1954 "targetPrice": target, # requested price for order execution in base currency 1955 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1956 "percentChanges": changes, # changes in percent to target from current price 1957 "currency": item["currency"], # instrument's currency name 1958 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1959 "type": orderType, # type of order from TKS_ORDER_TYPES 1960 "status": orderState, # order status from TKS_ORDER_STATES 1961 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1962 }) 1963 1964 # --- stop orders sector data: 1965 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1966 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1967 1968 for item in view["raw"]["stopOrders"]: 1969 self._figi = item["figi"] 1970 1971 if item["figi"] not in uniqueStopOrdersFIGIs: 1972 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1973 1974 uniqueStopOrdersFIGIs.append(item["figi"]) 1975 uniqueStopOrders[item["figi"]] = instrument 1976 1977 else: 1978 instrument = uniqueStopOrders[item["figi"]] 1979 1980 if instrument: 1981 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1982 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1983 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1984 1985 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1986 if "expirationTime" in item.keys(): 1987 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1988 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1989 1990 else: 1991 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1992 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1993 1994 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1995 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1996 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1997 1998 else: 1999 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2000 2001 # requested price when stop-order executed: 2002 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2003 2004 # price for limit-order, set up when stop-order executed: 2005 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2006 2007 # necessary changes in percent to reach target from current price: 2008 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2009 2010 view["stat"]["stopOrders"].append({ 2011 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2012 "figi": item["figi"], # FIGI identification 2013 "ticker": instrument["ticker"], # ticker name by FIGI 2014 "lotsRequested": item["lotsRequested"], # requested lots value 2015 "currentPrice": lastPrice, # current instrument's price for defined action 2016 "targetPrice": target, # requested price for stop-order execution in base currency 2017 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2018 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2019 "percentChanges": changes, # changes in percent to target from current price 2020 "currency": item["currency"], # instrument's currency name 2021 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2022 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2023 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2024 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2025 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2026 }) 2027 2028 # --- calculating data for analytics section: 2029 # portfolio distribution by assets: 2030 view["analytics"]["distrByAssets"] = { 2031 "Ruble": { 2032 "uniques": 1, 2033 "cost": view["stat"]["availableRUB"], 2034 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2035 }, 2036 "Currencies": { 2037 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2038 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2039 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2040 }, 2041 "Shares": { 2042 "uniques": len(view["stat"]["Shares"]), 2043 "cost": view["stat"]["sharesCostRUB"], 2044 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2045 }, 2046 "Bonds": { 2047 "uniques": len(view["stat"]["Bonds"]), 2048 "cost": view["stat"]["bondsCostRUB"], 2049 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2050 }, 2051 "Etfs": { 2052 "uniques": len(view["stat"]["Etfs"]), 2053 "cost": view["stat"]["etfsCostRUB"], 2054 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2055 }, 2056 "Futures": { 2057 "uniques": len(view["stat"]["Futures"]), 2058 "cost": view["stat"]["futuresCostRUB"], 2059 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2060 }, 2061 } 2062 2063 # portfolio distribution by companies: 2064 view["analytics"]["distrByCompanies"]["All money cash"] = { 2065 "ticker": "", 2066 "cost": view["stat"]["allCurrenciesCostRUB"], 2067 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2068 } 2069 view["analytics"]["distrByCompanies"].update(byComp) 2070 2071 # portfolio distribution by sectors: 2072 view["analytics"]["distrBySectors"]["All money cash"] = { 2073 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2074 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2075 } 2076 view["analytics"]["distrBySectors"].update(bySect) 2077 2078 # portfolio distribution by currencies: 2079 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2080 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2081 2082 if self.moreDebug: 2083 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2084 2085 view["analytics"]["distrByCurrencies"].update(byCurr) 2086 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2087 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2088 2089 # portfolio distribution by countries: 2090 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2091 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2092 2093 if self.moreDebug: 2094 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2095 2096 view["analytics"]["distrByCountries"].update(byCountry) 2097 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2098 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2099 2100 # --- Prepare text statistics overview in human-readable: 2101 if show or onlyFiles: 2102 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2103 2104 # Whatever the value `details`, header not changes: 2105 info = [ 2106 "# Client's portfolio\n\n", 2107 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2108 "* **Account ID:** [{}]\n".format(self.accountId), 2109 ] 2110 2111 if details in ["full", "positions", "digest"]: 2112 info.extend([ 2113 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2114 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2115 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2116 view["stat"]["totalChangesRUB"], 2117 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2118 view["stat"]["totalChangesPercentRUB"], 2119 ), 2120 ]) 2121 2122 if details in ["full", "positions"]: 2123 info.extend([ 2124 "## Open positions\n\n", 2125 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2126 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2127 "| **Ruble:** | {:>31} | | | | | |\n".format( 2128 "{:.2f} ({:.2f}) rub".format( 2129 view["stat"]["availableRUB"], 2130 view["stat"]["blockedRUB"], 2131 ) 2132 ) 2133 ]) 2134 2135 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2136 return [ 2137 "| | | | | | | |\n", 2138 "| {:<27} | | | | | {:>19} | |\n".format( 2139 noTradeStr if noTradeStr else typeStr, 2140 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2141 ), 2142 ] 2143 2144 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2145 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2146 "{} [{}]".format(data["ticker"], data["figi"]), 2147 "{:.2f} ({:.2f}) {}".format( 2148 data["volume"], 2149 data["blocked"], 2150 data["currency"], 2151 ) if isCurr else "{:.0f} ({:.0f})".format( 2152 data["volume"], 2153 data["blocked"], 2154 ), 2155 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2156 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2157 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2158 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2159 "{}{:.2f} {} ({}{:.2f}%)".format( 2160 "+" if data["profit"] > 0 else "", 2161 data["profit"], data["baseCurrencyName"], 2162 "+" if data["percentProfit"] > 0 else "", 2163 data["percentProfit"], 2164 ), 2165 ) 2166 2167 # --- Show currencies section: 2168 if view["stat"]["Currencies"]: 2169 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2170 for item in view["stat"]["Currencies"]: 2171 info.append(_InfoStr(item, isCurr=True)) 2172 2173 else: 2174 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2175 2176 # --- Show shares section: 2177 if view["stat"]["Shares"]: 2178 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2179 2180 for item in view["stat"]["Shares"]: 2181 info.append(_InfoStr(item)) 2182 2183 else: 2184 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2185 2186 # --- Show bonds section: 2187 if view["stat"]["Bonds"]: 2188 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2189 2190 for item in view["stat"]["Bonds"]: 2191 info.append(_InfoStr(item)) 2192 2193 else: 2194 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2195 2196 # --- Show etfs section: 2197 if view["stat"]["Etfs"]: 2198 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2199 2200 for item in view["stat"]["Etfs"]: 2201 info.append(_InfoStr(item)) 2202 2203 else: 2204 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2205 2206 # --- Show futures section: 2207 if view["stat"]["Futures"]: 2208 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2209 2210 for item in view["stat"]["Futures"]: 2211 info.append(_InfoStr(item)) 2212 2213 else: 2214 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2215 2216 if details in ["full", "orders"]: 2217 # --- Show pending limit orders section: 2218 if view["stat"]["orders"]: 2219 info.extend([ 2220 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2221 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2222 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2223 ]) 2224 2225 for item in view["stat"]["orders"]: 2226 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2227 "{} [{}]".format(item["ticker"], item["figi"]), 2228 item["orderID"], 2229 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2230 "{} {} ({}{:.2f}%)".format( 2231 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2232 item["baseCurrencyName"], 2233 "+" if item["percentChanges"] > 0 else "", 2234 float(item["percentChanges"]), 2235 ), 2236 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2237 item["action"], 2238 item["type"], 2239 item["date"], 2240 )) 2241 2242 else: 2243 info.append("\n## Total pending limit-orders: [0]\n") 2244 2245 # --- Show stop orders section: 2246 if view["stat"]["stopOrders"]: 2247 info.extend([ 2248 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2249 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2250 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2251 ]) 2252 2253 for item in view["stat"]["stopOrders"]: 2254 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2255 "{} [{}]".format(item["ticker"], item["figi"]), 2256 item["orderID"], 2257 item["lotsRequested"], 2258 "{} {} ({}{:.2f}%)".format( 2259 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2260 item["baseCurrencyName"], 2261 "+" if item["percentChanges"] > 0 else "", 2262 float(item["percentChanges"]), 2263 ), 2264 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2265 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2266 item["action"], 2267 item["type"], 2268 item["expType"], 2269 item["createDate"], 2270 item["expDate"], 2271 )) 2272 2273 else: 2274 info.append("\n## Total stop-orders: [0]\n") 2275 2276 if details in ["full", "analytics"]: 2277 # -- Show analytics section: 2278 if view["stat"]["portfolioCostRUB"] > 0: 2279 info.extend([ 2280 "\n# Analytics\n\n" 2281 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2282 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2283 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2284 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2285 view["stat"]["totalChangesRUB"], 2286 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2287 view["stat"]["totalChangesPercentRUB"], 2288 ), 2289 "\n## Portfolio distribution by assets\n" 2290 "\n| Type | Uniques | Percent | Current cost |\n", 2291 "|------------------------------------|---------|---------|--------------------|\n", 2292 ]) 2293 2294 for key in view["analytics"]["distrByAssets"].keys(): 2295 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2296 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2297 key, 2298 view["analytics"]["distrByAssets"][key]["uniques"], 2299 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2300 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2301 )) 2302 2303 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2304 2305 info.extend([ 2306 "\n## Portfolio distribution by companies\n" 2307 "\n| Company | Percent | Current cost |\n", 2308 aSepLine, 2309 ]) 2310 2311 for company in view["analytics"]["distrByCompanies"].keys(): 2312 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2313 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2314 "{}{}".format( 2315 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2316 company, 2317 ), 2318 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2319 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2320 )) 2321 2322 info.extend([ 2323 "\n## Portfolio distribution by sectors\n" 2324 "\n| Sector | Percent | Current cost |\n", 2325 aSepLine, 2326 ]) 2327 2328 for sector in view["analytics"]["distrBySectors"].keys(): 2329 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2330 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2331 sector, 2332 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2333 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2334 )) 2335 2336 info.extend([ 2337 "\n## Portfolio distribution by currencies\n" 2338 "\n| Instruments currencies | Percent | Current cost |\n", 2339 aSepLine, 2340 ]) 2341 2342 for curr in view["analytics"]["distrByCurrencies"].keys(): 2343 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2344 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2345 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2346 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2347 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2348 )) 2349 2350 info.extend([ 2351 "\n## Portfolio distribution by countries\n" 2352 "\n| Assets by country | Percent | Current cost |\n", 2353 aSepLine, 2354 ]) 2355 2356 for country in view["analytics"]["distrByCountries"].keys(): 2357 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2358 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2359 country, 2360 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2361 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2362 )) 2363 2364 if details in ["full", "calendar"]: 2365 # -- Show bonds payment calendar section: 2366 if view["stat"]["Bonds"]: 2367 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2368 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2369 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2370 2371 else: 2372 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2373 2374 infoText = "".join(info) 2375 2376 if show and not onlyFiles: 2377 uLogger.info(infoText) 2378 2379 if details == "full" and self.overviewFile: 2380 filename = self.overviewFile 2381 2382 elif details == "digest" and self.overviewDigestFile: 2383 filename = self.overviewDigestFile 2384 2385 elif details == "positions" and self.overviewPositionsFile: 2386 filename = self.overviewPositionsFile 2387 2388 elif details == "orders" and self.overviewOrdersFile: 2389 filename = self.overviewOrdersFile 2390 2391 elif details == "analytics" and self.overviewAnalyticsFile: 2392 filename = self.overviewAnalyticsFile 2393 2394 elif details == "calendar" and self.overviewBondsCalendarFile: 2395 filename = self.overviewBondsCalendarFile 2396 2397 else: 2398 filename = "" 2399 2400 if filename and (show or onlyFiles): 2401 with open(filename, "w", encoding="UTF-8") as fH: 2402 fH.write(infoText) 2403 2404 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2405 2406 if self.useHTMLReports: 2407 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2408 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2409 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2410 2411 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2412 2413 return view 2414 2415 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2416 """ 2417 Returns history operations between two given dates for current `accountId`. 2418 If `reportFile` string is not empty then also save human-readable report. 2419 Shows some statistical data of closed positions. 2420 2421 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2422 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2423 :param show: if `True` then also prints all records to the console. 2424 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2425 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2426 :return: original list of dictionaries with history of deals records from API ("operations" key): 2427 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2428 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2429 """ 2430 if self.accountId is None or not self.accountId: 2431 uLogger.error("Variable `accountId` must be defined for using this method!") 2432 raise Exception("Account ID required") 2433 2434 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2435 2436 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2437 2438 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2439 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2440 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2441 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2442 customStat = {} # custom statistics in additional to responseJSON 2443 2444 # --- output report in human-readable format: 2445 if show or onlyFiles or self.reportFile: 2446 splitLine1 = "| | | | | |\n" # Summary section 2447 splitLine2 = "| | | | | | | | |\n" # Operations section 2448 nextDay = "" 2449 2450 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2451 2452 if len(ops) > 0: 2453 customStat = { 2454 "opsCount": 0, # total operations count 2455 "buyCount": 0, # buy operations 2456 "sellCount": 0, # sell operations 2457 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2458 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2459 "payIn": {"rub": 0.}, # Deposit brokerage account 2460 "payOut": {"rub": 0.}, # Withdrawals 2461 "divs": {"rub": 0.}, # Dividends income 2462 "coupons": {"rub": 0.}, # Coupon's income 2463 "brokerCom": {"rub": 0.}, # Service commissions 2464 "serviceCom": {"rub": 0.}, # Service commissions 2465 "marginCom": {"rub": 0.}, # Margin commissions 2466 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2467 } 2468 2469 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2470 for item in ops: 2471 if item["state"] == "OPERATION_STATE_EXECUTED": 2472 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2473 2474 # count buy operations: 2475 if "_BUY" in item["operationType"]: 2476 customStat["buyCount"] += 1 2477 2478 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2479 customStat["buyTotal"][item["payment"]["currency"]] += payment 2480 2481 else: 2482 customStat["buyTotal"][item["payment"]["currency"]] = payment 2483 2484 # count sell operations: 2485 elif "_SELL" in item["operationType"]: 2486 customStat["sellCount"] += 1 2487 2488 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2489 customStat["sellTotal"][item["payment"]["currency"]] += payment 2490 2491 else: 2492 customStat["sellTotal"][item["payment"]["currency"]] = payment 2493 2494 # count incoming operations: 2495 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2496 if item["payment"]["currency"] in customStat["payIn"].keys(): 2497 customStat["payIn"][item["payment"]["currency"]] += payment 2498 2499 else: 2500 customStat["payIn"][item["payment"]["currency"]] = payment 2501 2502 # count withdrawals operations: 2503 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2504 if item["payment"]["currency"] in customStat["payOut"].keys(): 2505 customStat["payOut"][item["payment"]["currency"]] += payment 2506 2507 else: 2508 customStat["payOut"][item["payment"]["currency"]] = payment 2509 2510 # count dividends income: 2511 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2512 if item["payment"]["currency"] in customStat["divs"].keys(): 2513 customStat["divs"][item["payment"]["currency"]] += payment 2514 2515 else: 2516 customStat["divs"][item["payment"]["currency"]] = payment 2517 2518 # count coupon's income: 2519 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2520 if item["payment"]["currency"] in customStat["coupons"].keys(): 2521 customStat["coupons"][item["payment"]["currency"]] += payment 2522 2523 else: 2524 customStat["coupons"][item["payment"]["currency"]] = payment 2525 2526 # count broker commissions: 2527 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2528 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2529 customStat["brokerCom"][item["payment"]["currency"]] += payment 2530 2531 else: 2532 customStat["brokerCom"][item["payment"]["currency"]] = payment 2533 2534 # count service commissions: 2535 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2536 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2537 customStat["serviceCom"][item["payment"]["currency"]] += payment 2538 2539 else: 2540 customStat["serviceCom"][item["payment"]["currency"]] = payment 2541 2542 # count margin commissions: 2543 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2544 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2545 customStat["marginCom"][item["payment"]["currency"]] += payment 2546 2547 else: 2548 customStat["marginCom"][item["payment"]["currency"]] = payment 2549 2550 # count withholding taxes: 2551 elif "_TAX" in item["operationType"]: 2552 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2553 customStat["allTaxes"][item["payment"]["currency"]] += payment 2554 2555 else: 2556 customStat["allTaxes"][item["payment"]["currency"]] = payment 2557 2558 else: 2559 continue 2560 2561 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2562 2563 # --- view "Actions" lines: 2564 info.extend([ 2565 "| Report sections | | | | |\n", 2566 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2567 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2568 "| | Buy: {:<22} | {:<28} | | |\n".format( 2569 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2570 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2571 ), 2572 "| | Sell: {:<21} | {:<28} | | |\n".format( 2573 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2574 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2575 ), 2576 ]) 2577 2578 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2579 for key in opsKeys: 2580 if key == "rub": 2581 continue 2582 2583 info.extend([ 2584 "| | | {:<28} | | |\n".format( 2585 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2586 ), 2587 "| | | {:<28} | | |\n".format( 2588 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2589 ), 2590 ]) 2591 2592 info.append(splitLine1) 2593 2594 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2595 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2596 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2597 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2598 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2599 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2600 ) 2601 2602 # --- view "Payments" lines: 2603 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2604 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2605 2606 for key in paymentsKeys: 2607 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2608 2609 info.append(splitLine1) 2610 2611 # --- view "Commissions and taxes" lines: 2612 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2613 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2614 2615 for key in comKeys: 2616 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2617 2618 info.extend([ 2619 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2620 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2621 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2622 ]) 2623 2624 else: 2625 info.append("Broker returned no operations during this period\n") 2626 2627 # --- view "Operations" section: 2628 for item in ops: 2629 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2630 continue 2631 2632 else: 2633 self._figi = item["figi"] 2634 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2635 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2636 2637 # group of deals during one day: 2638 if nextDay and item["date"].split("T")[0] != nextDay: 2639 info.append(splitLine2) 2640 nextDay = "" 2641 2642 else: 2643 nextDay = item["date"].split("T")[0] # saving current day for splitting 2644 2645 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2646 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2647 self._figi if self._figi else "—", 2648 instrument["ticker"] if instrument else "—", 2649 instrument["type"] if instrument else "—", 2650 item["quantity"] if int(item["quantity"]) > 0 else "—", 2651 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2652 TKS_OPERATION_STATES[item["state"]], 2653 TKS_OPERATION_TYPES[item["operationType"]], 2654 )) 2655 2656 infoText = "".join(info) 2657 2658 if show and not onlyFiles: 2659 if self.moreDebug: 2660 uLogger.debug("Records about history of a client's operations successfully received") 2661 2662 uLogger.info(infoText) 2663 2664 if self.reportFile and (show or onlyFiles): 2665 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2666 fH.write(infoText) 2667 2668 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2669 2670 if self.useHTMLReports: 2671 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2672 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2673 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2674 2675 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2676 2677 return ops, customStat 2678 2679 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2680 """ 2681 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2682 2683 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2684 Warning! Broker server used ISO UTC time by default. 2685 2686 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2687 Also, `historyFile` used to update history with `onlyMissing` parameter. 2688 2689 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2690 2691 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2692 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2693 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2694 `"hour"`, `"day"`. Default: `"hour"`. 2695 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2696 False by default. Warning! History appends only from last candle to current time 2697 with always update last candle! 2698 :param csvSep: separator if csv-file is used, `,` by default. 2699 :param show: if `True` then also prints Pandas DataFrame to the console. 2700 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2701 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2702 `["date", "time", "open", "high", "low", "close", "volume"]`. 2703 """ 2704 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2705 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2706 history = None # empty pandas object for history 2707 2708 if interval not in TKS_CANDLE_INTERVALS.keys(): 2709 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2710 raise Exception("Incorrect value") 2711 2712 if not (self._ticker or self._figi): 2713 uLogger.error("Ticker or FIGI must be defined!") 2714 raise Exception("Ticker or FIGI required") 2715 2716 if self._ticker and not self._figi: 2717 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2718 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2719 2720 if self._figi and not self._ticker: 2721 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2722 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2723 2724 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2725 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2726 if interval.lower() != "day": 2727 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2728 2729 delta = dtEnd - dtStart # current UTC time minus last time in file 2730 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2731 2732 # calculate history length in candles: 2733 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2734 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2735 length += 1 # to avoid fraction time 2736 2737 # calculate data blocks count: 2738 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2739 2740 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2741 if self.moreDebug: 2742 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2743 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2744 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2745 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2746 2747 tempOld = None # pandas object for old history, if --only-missing key present 2748 lastTime = None # datetime object of last old candle in file 2749 2750 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2751 if self.moreDebug: 2752 uLogger.debug("--only-missing key present, add only last missing candles...") 2753 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2754 2755 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2756 2757 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2758 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2759 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2760 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2761 2762 # get last datetime object from last string in file or minus 1 delta if file is empty: 2763 if len(tempOld) > 0: 2764 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2765 2766 else: 2767 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2768 2769 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2770 2771 responseJSONs = [] # raw history blocks of data 2772 2773 blockEnd = dtEnd 2774 for item in range(blocks): 2775 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2776 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2777 2778 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2779 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2780 )) 2781 2782 if blockStart == blockEnd: 2783 uLogger.debug("Skipped this zero-length block...") 2784 2785 else: 2786 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2787 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2788 self.body = str({ 2789 "figi": self._figi, 2790 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2791 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2792 "interval": TKS_CANDLE_INTERVALS[interval][0] 2793 }) 2794 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2795 2796 if "code" in responseJSON.keys(): 2797 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2798 2799 else: 2800 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2801 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2802 2803 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2804 2805 blockEnd = blockStart 2806 2807 printCount = len(responseJSONs) # candles to show in console 2808 if responseJSONs: 2809 tempHistory = pd.DataFrame( 2810 data={ 2811 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2812 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2813 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2814 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2815 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2816 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2817 "volume": [int(item["volume"]) for item in responseJSONs], 2818 }, 2819 index=range(len(responseJSONs)), 2820 columns=["date", "time", "open", "high", "low", "close", "volume"], 2821 ) 2822 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2823 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2824 2825 # append only newest candles to old history if --only-missing key present: 2826 if onlyMissing and tempOld is not None and lastTime is not None: 2827 index = 0 # find start index in tempHistory data: 2828 2829 for i, item in tempHistory.iterrows(): 2830 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2831 2832 if curTime == lastTime: 2833 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2834 index = i 2835 printCount = index + 1 2836 break 2837 2838 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2839 2840 else: 2841 history = tempHistory # if no `--only-missing` key then load full data from server 2842 2843 if self.moreDebug: 2844 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2845 2846 if history is not None and not history.empty: 2847 if show and not onlyFiles: 2848 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2849 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2850 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2851 )) 2852 2853 else: 2854 uLogger.warning("Received an empty candles history!") 2855 2856 if self.historyFile is not None: 2857 if history is not None and not history.empty: 2858 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2859 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2860 2861 else: 2862 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2863 2864 else: 2865 if self.moreDebug: 2866 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2867 2868 return history 2869 2870 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2871 """ 2872 Load candles history from csv-file and return Pandas DataFrame object. 2873 2874 See also: `History()` and `ShowHistoryChart()` methods. 2875 2876 :param filePath: path to csv-file to open. 2877 """ 2878 loadedHistory = None # init candles data object 2879 2880 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2881 2882 if os.path.exists(filePath): 2883 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2884 2885 tfStr = self.priceModel.FormattedDelta( 2886 self.priceModel.timeframe, 2887 "{days} days {hours}h {minutes}m {seconds}s", 2888 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2889 self.priceModel.timeframe, 2890 "{hours}h {minutes}m {seconds}s", 2891 ) 2892 2893 if loadedHistory is not None and not loadedHistory.empty: 2894 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2895 len(loadedHistory), 2896 tfStr, 2897 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2898 ) 2899 2900 else: 2901 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2902 2903 else: 2904 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2905 2906 return loadedHistory 2907 2908 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2909 """ 2910 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2911 2912 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2913 Default: `index.html` (both for interact and non-interact candlesticks chart). 2914 2915 See also: `History()` and `LoadHistory()` methods. 2916 2917 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2918 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2919 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2920 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2921 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2922 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2923 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2924 """ 2925 if isinstance(candles, str): 2926 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2927 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2928 2929 elif isinstance(candles, pd.DataFrame): 2930 self.priceModel.prices = candles # set candles chain from variable 2931 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2932 2933 if "datetime" not in candles.columns: 2934 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2935 2936 else: 2937 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2938 raise Exception("Incorrect value") 2939 2940 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2941 2942 if interact: 2943 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2944 2945 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2946 2947 else: 2948 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2949 2950 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2951 2952 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2953 2954 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2955 """ 2956 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2957 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2958 2959 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2960 2961 :param operation: string "Buy" or "Sell". 2962 :param lots: volume, integer count of lots >= 1. 2963 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2964 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2965 :param expDate: string "Undefined" by default or local date in future, 2966 it is a string with format `%Y-%m-%d %H:%M:%S`. 2967 :return: JSON with response from broker server. 2968 """ 2969 if self.accountId is None or not self.accountId: 2970 uLogger.error("Variable `accountId` must be defined for using this method!") 2971 raise Exception("Account ID required") 2972 2973 if operation is None or not operation or operation not in ("Buy", "Sell"): 2974 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2975 raise Exception("Incorrect value") 2976 2977 if lots is None or lots < 1: 2978 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2979 lots = 1 2980 2981 if tp is None or tp < 0: 2982 tp = 0 2983 2984 if sl is None or sl < 0: 2985 sl = 0 2986 2987 if expDate is None or not expDate: 2988 expDate = "Undefined" 2989 2990 if not (self._ticker or self._figi): 2991 uLogger.error("Ticker or FIGI must be defined!") 2992 raise Exception("Ticker or FIGI required") 2993 2994 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2995 self._ticker = instrument["ticker"] 2996 self._figi = instrument["figi"] 2997 2998 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2999 3000 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3001 self.body = str({ 3002 "figi": self._figi, 3003 "quantity": str(lots), 3004 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3005 "accountId": str(self.accountId), 3006 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3007 }) 3008 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3009 3010 if "orderId" in response.keys(): 3011 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3012 operation, response["orderId"], 3013 self._ticker, self._figi, lots, 3014 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3015 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3016 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3017 )) 3018 3019 if tp > 0: 3020 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3021 3022 if sl > 0: 3023 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3024 3025 else: 3026 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3027 3028 return response 3029 3030 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3031 """ 3032 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3033 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3034 3035 See also: `Order()` and `Trade()` docstrings. 3036 3037 :param lots: volume, integer count of lots >= 1. 3038 :param tp: float > 0, take profit price of stop-order. 3039 :param sl: float > 0, stop loss price of stop-order. 3040 :param expDate: it's a local date in future. 3041 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3042 :return: JSON with response from broker server. 3043 """ 3044 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3045 3046 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3047 """ 3048 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3049 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3050 3051 See also: `Order()` and `Trade()` docstrings. 3052 3053 :param lots: volume, integer count of lots >= 1. 3054 :param tp: float > 0, take profit price of stop-order. 3055 :param sl: float > 0, stop loss price of stop-order. 3056 :param expDate: it's a local date in the future. 3057 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3058 :return: JSON with response from broker server. 3059 """ 3060 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3061 3062 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3063 """ 3064 Close position of given instruments. 3065 3066 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3067 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3068 This avoids unnecessary downloading data from the server. 3069 """ 3070 if instruments is None or not instruments: 3071 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3072 raise Exception("Ticker or FIGI required") 3073 3074 if isinstance(instruments, str): 3075 instruments = [instruments] 3076 3077 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3078 if uniqueInstruments: 3079 if portfolio is None or not portfolio: 3080 portfolio = self.Overview(show=False) 3081 3082 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3083 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3084 3085 for self._figi in uniqueInstruments: 3086 if self._figi not in allOpened: 3087 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3088 continue 3089 3090 # search open trade info about instrument by ticker: 3091 instrument = {} 3092 for iType in TKS_INSTRUMENTS: 3093 if instrument: 3094 break 3095 3096 for item in portfolio["stat"][iType]: 3097 if item["figi"] == self._figi: 3098 instrument = item 3099 break 3100 3101 if instrument: 3102 self._ticker = instrument["ticker"] 3103 self._figi = instrument["figi"] 3104 3105 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3106 self._ticker, 3107 self._figi, 3108 int(instrument["volume"]), 3109 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3110 )) 3111 3112 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3113 3114 if tradeLots > 0: 3115 if instrument["blocked"] > 0: 3116 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3117 instrument["blocked"], 3118 self._ticker, 3119 tradeLots, 3120 )) 3121 3122 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3123 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3124 3125 else: 3126 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3127 3128 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3129 """ 3130 Close all positions of given instruments with defined type. 3131 3132 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3133 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3134 This avoids unnecessary downloading data from the server. 3135 """ 3136 if iType not in TKS_INSTRUMENTS: 3137 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3138 3139 else: 3140 if portfolio is None or not portfolio: 3141 portfolio = self.Overview(show=False) 3142 3143 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3144 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3145 3146 if tickers and portfolio: 3147 self.CloseTrades(tickers, portfolio) 3148 3149 else: 3150 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3151 3152 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3153 """ 3154 Universal method to create market or limit orders with all available parameters for current `accountId`. 3155 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3156 3157 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3158 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3159 3160 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3161 then broker immediately open market order as you can do simple --buy or --sell operations! 3162 3163 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3164 When current price will go up or down to target price value then broker opens a limit order. 3165 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3166 3167 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3168 3169 :param operation: string "Buy" or "Sell". 3170 :param orderType: string "Limit" or "Stop". 3171 :param lots: volume, integer count of lots >= 1. 3172 :param targetPrice: target price > 0. This is open trade price for limit order. 3173 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3174 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3175 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3176 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3177 Stop loss order always executed by market price. 3178 :param expDate: string "Undefined" by default or local date in future. 3179 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3180 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3181 A limit order has no expiration date, it lasts until the end of the trading day. 3182 :return: JSON with response from broker server. 3183 """ 3184 if self.accountId is None or not self.accountId: 3185 uLogger.error("Variable `accountId` must be defined for using this method!") 3186 raise Exception("Account ID required") 3187 3188 if operation is None or not operation or operation not in ("Buy", "Sell"): 3189 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3190 raise Exception("Incorrect value") 3191 3192 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3193 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3194 raise Exception("Incorrect value") 3195 3196 if lots is None or lots < 1: 3197 uLogger.error("You must define trade volume > 0: integer count of lots!") 3198 raise Exception("Incorrect value") 3199 3200 if targetPrice is None or targetPrice <= 0: 3201 uLogger.error("Target price for limit-order must be greater than 0!") 3202 raise Exception("Incorrect value") 3203 3204 if limitPrice is None or limitPrice <= 0: 3205 limitPrice = targetPrice 3206 3207 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3208 stopType = "Limit" 3209 3210 if expDate is None or not expDate: 3211 expDate = "Undefined" 3212 3213 if not (self._ticker or self._figi): 3214 uLogger.error("Tocker or FIGI must be defined!") 3215 raise Exception("Ticker or FIGI required") 3216 3217 response = {} 3218 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3219 self._ticker = instrument["ticker"] 3220 self._figi = instrument["figi"] 3221 3222 if orderType == "Limit": 3223 uLogger.debug( 3224 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3225 self._ticker, self._figi, 3226 operation, lots, targetPrice, instrument["currency"], 3227 )) 3228 3229 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3230 self.body = str({ 3231 "figi": self._figi, 3232 "quantity": str(lots), 3233 "price": FloatToNano(targetPrice), 3234 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3235 "accountId": str(self.accountId), 3236 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3237 }) 3238 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3239 3240 if "orderId" in response.keys(): 3241 uLogger.info( 3242 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3243 response["orderId"], self._ticker, self._figi, operation, lots, 3244 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3245 )) 3246 3247 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3248 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3249 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3250 targetPrice, instrument["currency"], 3251 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3252 )) 3253 3254 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3255 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3256 targetPrice, instrument["currency"], 3257 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3258 )) 3259 3260 else: 3261 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3262 3263 if orderType == "Stop": 3264 uLogger.debug( 3265 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3266 self._ticker, self._figi, 3267 operation, lots, 3268 targetPrice, instrument["currency"], 3269 limitPrice, instrument["currency"], 3270 stopType, expDate, 3271 )) 3272 3273 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3274 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3275 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3276 3277 body = { 3278 "figi": self._figi, 3279 "quantity": str(lots), 3280 "price": FloatToNano(limitPrice), 3281 "stopPrice": FloatToNano(targetPrice), 3282 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3283 "accountId": str(self.accountId), 3284 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3285 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3286 } 3287 3288 if expDateUTC: 3289 body["expireDate"] = expDateUTC 3290 3291 self.body = str(body) 3292 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3293 3294 if "stopOrderId" in response.keys(): 3295 uLogger.info( 3296 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3297 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3298 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3299 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3300 TKS_STOP_ORDER_TYPES[stopOrderType], 3301 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3302 )) 3303 3304 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3305 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3306 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3307 targetPrice, instrument["currency"], 3308 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3309 )) 3310 3311 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3312 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3313 targetPrice, instrument["currency"], 3314 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3315 )) 3316 3317 else: 3318 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3319 3320 return response 3321 3322 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3323 """ 3324 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3325 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3326 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3327 See also: `Order()` docstring. 3328 3329 :param lots: volume, integer count of lots >= 1. 3330 :param targetPrice: target price > 0. This is open trade price for limit order. 3331 :return: JSON with response from broker server. 3332 """ 3333 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3334 3335 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3336 """ 3337 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3338 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3339 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3340 target price value then broker opens a limit order. See also: `Order()` docstring. 3341 3342 :param lots: volume, integer count of lots >= 1. 3343 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3344 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3345 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3346 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3347 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3348 :param expDate: string "Undefined" by default or local date in future. 3349 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3350 This date is converting to UTC format for server. 3351 :return: JSON with response from broker server. 3352 """ 3353 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3354 3355 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3356 """ 3357 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3358 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3359 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3360 See also: `Order()` docstring. 3361 3362 :param lots: volume, integer count of lots >= 1. 3363 :param targetPrice: target price > 0. This is open trade price for limit order. 3364 :return: JSON with response from broker server. 3365 """ 3366 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3367 3368 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3369 """ 3370 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3371 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3372 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3373 target price value then broker opens a limit order. See also: `Order()` docstring. 3374 3375 :param lots: volume, integer count of lots >= 1. 3376 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3377 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3378 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3379 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3380 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3381 :param expDate: string "Undefined" by default or local date in future. 3382 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3383 This date is converting to UTC format for server. 3384 :return: JSON with response from broker server. 3385 """ 3386 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3387 3388 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3389 """ 3390 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3391 3392 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3393 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3394 This avoids unnecessary downloading data from the server. 3395 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3396 """ 3397 if self.accountId is None or not self.accountId: 3398 uLogger.error("Variable `accountId` must be defined for using this method!") 3399 raise Exception("Account ID required") 3400 3401 if orderIDs: 3402 if allOrdersIDs is None: 3403 rawOrders = self.RequestPendingOrders() 3404 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3405 3406 if allStopOrdersIDs is None: 3407 rawStopOrders = self.RequestStopOrders() 3408 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3409 3410 for orderID in orderIDs: 3411 idInPendingOrders = orderID in allOrdersIDs 3412 idInStopOrders = orderID in allStopOrdersIDs 3413 3414 if not (idInPendingOrders or idInStopOrders): 3415 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3416 continue 3417 3418 else: 3419 if idInPendingOrders: 3420 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3421 3422 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3423 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3424 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3425 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3426 3427 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3428 if self.moreDebug: 3429 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3430 3431 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3432 3433 else: 3434 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3435 3436 elif idInStopOrders: 3437 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3438 3439 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3440 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3441 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3442 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3443 3444 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3445 if self.moreDebug: 3446 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3447 3448 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3449 3450 else: 3451 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3452 3453 else: 3454 continue 3455 3456 def CloseAllOrders(self) -> None: 3457 """ 3458 Gets a list of open pending and stop orders and cancel it all. 3459 """ 3460 rawOrders = self.RequestPendingOrders() 3461 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3462 lenOrders = len(allOrdersIDs) 3463 3464 rawStopOrders = self.RequestStopOrders() 3465 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3466 lenSOrders = len(allStopOrdersIDs) 3467 3468 if lenOrders > 0 or lenSOrders > 0: 3469 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3470 3471 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3472 3473 else: 3474 uLogger.info("Orders not found, nothing to cancel.") 3475 3476 def CloseAll(self, *args) -> None: 3477 """ 3478 Close all available (not blocked) opened trades and orders. 3479 3480 Also, you can select one or more keywords case-insensitive: 3481 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3482 3483 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3484 """ 3485 overview = self.Overview(show=False) # get all open trades info 3486 3487 if len(args) == 0: 3488 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3489 self.CloseAllOrders() # close all pending and stop orders 3490 3491 for iType in TKS_INSTRUMENTS: 3492 if iType != "Currencies": 3493 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3494 3495 else: 3496 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3497 lowerArgs = [x.lower() for x in args] 3498 3499 if "orders" in lowerArgs: 3500 self.CloseAllOrders() # close all pending and stop orders 3501 3502 for iType in TKS_INSTRUMENTS: 3503 if iType.lower() in lowerArgs and iType != "Currencies": 3504 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3505 3506 def CloseAllByTicker(self, instrument: str) -> None: 3507 """ 3508 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3509 3510 This method searches opened trade and orders of instrument throw all portfolio and then use 3511 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3512 3513 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3514 3515 :param instrument: string with ticker. 3516 """ 3517 if instrument is None or not instrument: 3518 uLogger.error("Ticker name must be defined for using this method!") 3519 raise Exception("Ticker required") 3520 3521 overview = self.Overview(show=False) # get user portfolio with all open trades info 3522 3523 self._ticker = instrument # try to set instrument as ticker 3524 self._figi = "" 3525 3526 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3527 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3528 3529 if limitAll and self.IsInLimitOrders(portfolio=overview): 3530 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3531 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3532 3533 if stopAll and self.IsInStopOrders(portfolio=overview): 3534 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3535 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3536 3537 if self.IsInPortfolio(portfolio=overview): 3538 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3539 self.CloseTrades(instruments=[instrument], portfolio=overview) 3540 3541 def CloseAllByFIGI(self, instrument: str) -> None: 3542 """ 3543 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3544 3545 This method searches opened trade and orders of instrument throw all portfolio and then use 3546 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3547 3548 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3549 3550 :param instrument: string with FIGI id. 3551 """ 3552 if instrument is None or not instrument: 3553 uLogger.error("FIGI id must be defined for using this method!") 3554 raise Exception("FIGI required") 3555 3556 overview = self.Overview(show=False) # get user portfolio with all open trades info 3557 3558 self._ticker = "" 3559 self._figi = instrument # try to set instrument as FIGI id 3560 3561 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3562 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3563 3564 if limitAll and self.IsInLimitOrders(portfolio=overview): 3565 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3566 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3567 3568 if stopAll and self.IsInStopOrders(portfolio=overview): 3569 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3570 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3571 3572 if self.IsInPortfolio(portfolio=overview): 3573 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3574 self.CloseTrades(instruments=[instrument], portfolio=overview) 3575 3576 @staticmethod 3577 def ParseOrderParameters(operation, **inputParameters): 3578 """ 3579 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3580 3581 :param operation: string "Buy" or "Sell". 3582 :param inputParameters: this is dict of strings that looks like this 3583 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3584 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3585 "prices" key: one or more prices to open limit-orders 3586 Counts of values in lots and prices lists must be equals! 3587 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3588 """ 3589 # TODO: update order grid work with api v2 3590 pass 3591 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3592 # 3593 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3594 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3595 # raise Exception("Incorrect value") 3596 # 3597 # if "l" in inputParameters.keys(): 3598 # inputParameters["lots"] = inputParameters.pop("l") 3599 # 3600 # if "p" in inputParameters.keys(): 3601 # inputParameters["prices"] = inputParameters.pop("p") 3602 # 3603 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3604 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3605 # raise Exception("Incorrect value") 3606 # 3607 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3608 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3609 # 3610 # if len(lots) != len(prices): 3611 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3612 # raise Exception("Incorrect value") 3613 # 3614 # uLogger.debug("Extracted parameters for orders:") 3615 # uLogger.debug("lots = {}".format(lots)) 3616 # uLogger.debug("prices = {}".format(prices)) 3617 # 3618 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3619 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3620 # uLogger.debug("Order parameters: {}".format(result)) 3621 # 3622 # return result 3623 3624 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3625 """ 3626 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3627 3628 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3629 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3630 """ 3631 result = False 3632 msg = "Instrument not defined!" 3633 3634 if portfolio is None or not portfolio: 3635 portfolio = self.Overview(show=False) 3636 3637 if self._ticker: 3638 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3639 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3640 3641 for iType in TKS_INSTRUMENTS: 3642 for instrument in portfolio["stat"][iType]: 3643 if instrument["ticker"] == self._ticker: 3644 result = True 3645 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3646 break 3647 3648 elif self._figi: 3649 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3650 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3651 3652 for iType in TKS_INSTRUMENTS: 3653 for instrument in portfolio["stat"][iType]: 3654 if instrument["figi"] == self._figi: 3655 result = True 3656 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3657 break 3658 3659 else: 3660 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3661 3662 uLogger.debug(msg) 3663 3664 return result 3665 3666 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3667 """ 3668 Returns instrument from the user's portfolio if it presents there. 3669 Instrument must be defined by `ticker` (highly priority) or `figi`. 3670 3671 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3672 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3673 """ 3674 result = None 3675 msg = "Instrument not defined!" 3676 3677 if portfolio is None or not portfolio: 3678 portfolio = self.Overview(show=False) 3679 3680 if self._ticker: 3681 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3682 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3683 3684 for iType in TKS_INSTRUMENTS: 3685 for instrument in portfolio["stat"][iType]: 3686 if instrument["ticker"] == self._ticker: 3687 result = instrument 3688 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3689 break 3690 3691 elif self._figi: 3692 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3693 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3694 3695 for iType in TKS_INSTRUMENTS: 3696 for instrument in portfolio["stat"][iType]: 3697 if instrument["figi"] == self._figi: 3698 result = instrument 3699 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3700 break 3701 3702 else: 3703 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3704 3705 uLogger.debug(msg) 3706 3707 return result 3708 3709 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3710 """ 3711 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3712 3713 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3714 3715 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3716 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3717 """ 3718 result = False 3719 msg = "Instrument not defined!" 3720 3721 if portfolio is None or not portfolio: 3722 portfolio = self.Overview(show=False) 3723 3724 if self._ticker: 3725 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3726 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3727 3728 for instrument in portfolio["stat"]["orders"]: 3729 if instrument["ticker"] == self._ticker: 3730 result = True 3731 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3732 break 3733 3734 elif self._figi: 3735 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3736 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3737 3738 for instrument in portfolio["stat"]["orders"]: 3739 if instrument["figi"] == self._figi: 3740 result = True 3741 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3742 break 3743 3744 else: 3745 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3746 3747 uLogger.debug(msg) 3748 3749 return result 3750 3751 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3752 """ 3753 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3754 Instrument must be defined by `ticker` (highly priority) or `figi`. 3755 3756 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3757 3758 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3759 :return: list with `orderID`s of limit orders. 3760 """ 3761 result = [] 3762 msg = "Instrument not defined!" 3763 3764 if portfolio is None or not portfolio: 3765 portfolio = self.Overview(show=False) 3766 3767 if self._ticker: 3768 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3769 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3770 3771 for instrument in portfolio["stat"]["orders"]: 3772 if instrument["ticker"] == self._ticker: 3773 result.append(instrument["orderID"]) 3774 3775 if result: 3776 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3777 3778 elif self._figi: 3779 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3780 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3781 3782 for instrument in portfolio["stat"]["orders"]: 3783 if instrument["figi"] == self._figi: 3784 result.append(instrument["orderID"]) 3785 3786 if result: 3787 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3788 3789 else: 3790 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3791 3792 uLogger.debug(msg) 3793 3794 return result 3795 3796 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3797 """ 3798 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3799 3800 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3801 3802 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3803 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3804 """ 3805 result = False 3806 msg = "Instrument not defined!" 3807 3808 if portfolio is None or not portfolio: 3809 portfolio = self.Overview(show=False) 3810 3811 if self._ticker: 3812 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3813 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3814 3815 for instrument in portfolio["stat"]["stopOrders"]: 3816 if instrument["ticker"] == self._ticker: 3817 result = True 3818 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3819 break 3820 3821 elif self._figi: 3822 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3823 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3824 3825 for instrument in portfolio["stat"]["stopOrders"]: 3826 if instrument["figi"] == self._figi: 3827 result = True 3828 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3829 break 3830 3831 else: 3832 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3833 3834 uLogger.debug(msg) 3835 3836 return result 3837 3838 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3839 """ 3840 Returns list with all `orderID`s of opened stop orders for the instrument. 3841 Instrument must be defined by `ticker` (highly priority) or `figi`. 3842 3843 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3844 3845 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3846 :return: list with `orderID`s of stop orders. 3847 """ 3848 result = [] 3849 msg = "Instrument not defined!" 3850 3851 if portfolio is None or not portfolio: 3852 portfolio = self.Overview(show=False) 3853 3854 if self._ticker: 3855 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3856 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3857 3858 for instrument in portfolio["stat"]["stopOrders"]: 3859 if instrument["ticker"] == self._ticker: 3860 result.append(instrument["orderID"]) 3861 3862 if result: 3863 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3864 3865 elif self._figi: 3866 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3867 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3868 3869 for instrument in portfolio["stat"]["stopOrders"]: 3870 if instrument["figi"] == self._figi: 3871 result.append(instrument["orderID"]) 3872 3873 if result: 3874 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3875 3876 else: 3877 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3878 3879 uLogger.debug(msg) 3880 3881 return result 3882 3883 def RequestLimits(self) -> dict: 3884 """ 3885 Method for obtaining the available funds for withdrawal for current `accountId`. 3886 3887 See also: 3888 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3889 - `OverviewLimits()` method 3890 3891 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3892 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3893 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3894 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3895 """ 3896 if self.accountId is None or not self.accountId: 3897 uLogger.error("Variable `accountId` must be defined for using this method!") 3898 raise Exception("Account ID required") 3899 3900 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3901 3902 self.body = str({"accountId": self.accountId}) 3903 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3904 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3905 3906 if self.moreDebug: 3907 uLogger.debug("Records about available funds for withdrawal successfully received") 3908 3909 return rawLimits 3910 3911 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3912 """ 3913 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3914 3915 See also: `RequestLimits()`. 3916 3917 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3918 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3919 :return: dict with raw parsed data from server and some calculated statistics about it. 3920 """ 3921 if self.accountId is None or not self.accountId: 3922 uLogger.error("Variable `accountId` must be defined for using this method!") 3923 raise Exception("Account ID required") 3924 3925 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3926 3927 view = { 3928 "rawLimits": rawLimits, 3929 "limits": { # parsed data for every currency: 3930 "money": { # this is an array of portfolio currency positions 3931 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3932 }, 3933 "blocked": { # this is an array of blocked currency 3934 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3935 }, 3936 "blockedGuarantee": { # this is locked money under collateral for futures 3937 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3938 }, 3939 }, 3940 } 3941 3942 # --- Prepare text table with limits in human-readable format: 3943 if show or onlyFiles: 3944 info = [ 3945 "# Withdrawal limits\n\n", 3946 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3947 "* **Account ID:** [{}]\n".format(self.accountId), 3948 ] 3949 3950 if view["limits"]["money"]: 3951 info.extend([ 3952 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3953 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3954 ]) 3955 3956 else: 3957 info.append("\nNo withdrawal limits\n") 3958 3959 for curr in view["limits"]["money"].keys(): 3960 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3961 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3962 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3963 3964 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3965 "[{}]".format(curr), 3966 "{:.2f}".format(view["limits"]["money"][curr]), 3967 "{:.2f}".format(availableMoney), 3968 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3969 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3970 ) 3971 3972 if curr == "rub": 3973 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3974 3975 else: 3976 info.append(infoStr) 3977 3978 infoText = "".join(info) 3979 3980 if show and not onlyFiles: 3981 uLogger.info(infoText) 3982 3983 if self.withdrawalLimitsFile and (show or onlyFiles): 3984 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3985 fH.write(infoText) 3986 3987 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3988 3989 if self.useHTMLReports: 3990 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3991 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3992 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3993 3994 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3995 3996 return view 3997 3998 def RequestAccounts(self) -> dict: 3999 """ 4000 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4001 4002 See also: 4003 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4004 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4005 - `OverviewUserInfo()` method 4006 4007 :return: dict with raw data from server that contains accounts info. Example of dict: 4008 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4009 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4010 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4011 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4012 """ 4013 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4014 4015 self.body = str({}) 4016 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4017 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4018 4019 if self.moreDebug: 4020 uLogger.debug("Records about available accounts successfully received") 4021 4022 return rawAccounts 4023 4024 def RequestUserInfo(self) -> dict: 4025 """ 4026 Method for requesting common user's information. 4027 4028 See also: 4029 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4030 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4031 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4032 - `OverviewUserInfo()` method 4033 4034 :return: dict with raw data from server that contains user's information. Example of dict: 4035 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4036 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4037 """ 4038 uLogger.debug("Requesting common user's information. Wait, please...") 4039 4040 self.body = str({}) 4041 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4042 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4043 4044 if self.moreDebug: 4045 uLogger.debug("Records about current user successfully received") 4046 4047 return rawUserInfo 4048 4049 def RequestMarginStatus(self, accountId: str = None) -> dict: 4050 """ 4051 Method for requesting margin calculation for defined account ID. 4052 4053 See also: 4054 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4055 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4056 - `OverviewUserInfo()` method 4057 4058 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4059 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4060 Example of responses: 4061 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4062 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4063 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4064 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4065 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4066 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4067 """ 4068 if accountId is None or not accountId: 4069 if self.accountId is None or not self.accountId: 4070 uLogger.error("Variable `accountId` must be defined for using this method!") 4071 raise Exception("Account ID required") 4072 4073 else: 4074 accountId = self.accountId # use `self.accountId` (main ID) by default 4075 4076 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4077 4078 self.body = str({"accountId": accountId}) 4079 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4080 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4081 4082 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4083 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4084 rawMargin = {} 4085 4086 else: 4087 if self.moreDebug: 4088 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4089 4090 return rawMargin 4091 4092 def RequestTariffLimits(self) -> dict: 4093 """ 4094 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4095 4096 See also: 4097 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4098 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4099 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4100 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4101 - `OverviewUserInfo()` method 4102 4103 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4104 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4105 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4106 """ 4107 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4108 4109 self.body = str({}) 4110 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4111 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4112 4113 if self.moreDebug: 4114 uLogger.debug("Records with limits of current tariff successfully received") 4115 4116 return rawTariffLimits 4117 4118 def RequestBondCoupons(self, iJSON: dict) -> dict: 4119 """ 4120 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4121 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4122 All dates are in UTC timezone. 4123 4124 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4125 Documentation: 4126 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4127 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4128 4129 See also: `ExtendBondsData()`. 4130 4131 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4132 If raw iJSON is not data of bond then server returns an error [400] with message: 4133 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4134 :return: dictionary with bond payment calendar. Response example 4135 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4136 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4137 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4138 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4139 """ 4140 if iJSON["figi"] is None or not iJSON["figi"]: 4141 uLogger.error("FIGI must be defined for using this method!") 4142 raise Exception("FIGI required") 4143 4144 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4145 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4146 4147 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4148 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4149 self._figi, 4150 startDate, 4151 endDate, 4152 )) 4153 4154 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4155 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4156 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4157 4158 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4159 uLogger.warning("Instrument type is not bond!") 4160 4161 else: 4162 if self.moreDebug: 4163 uLogger.debug("Records about bond payment calendar successfully received") 4164 4165 return calendar 4166 4167 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4168 """ 4169 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4170 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4171 coupon yields, current yields and some statistics etc. 4172 4173 WARNING! This is too long operation if a lot of bonds requested from broker server. 4174 4175 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4176 4177 :param instruments: list of strings with tickers or FIGIs. 4178 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4179 for further used by data scientists or stock analytics. 4180 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4181 In XLSX-file and Pandas DataFrame fields mean: 4182 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4183 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4184 """ 4185 if instruments is None or not instruments: 4186 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4187 raise Exception("Ticker or FIGI required") 4188 4189 if isinstance(instruments, str): 4190 instruments = [instruments] 4191 4192 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4193 4194 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4195 4196 iCount = len(uniqueInstruments) 4197 tooLong = iCount >= 20 4198 if tooLong: 4199 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4200 4201 bonds = None 4202 for i, self._figi in enumerate(uniqueInstruments): 4203 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4204 4205 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4206 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4207 rawBond = self.SearchByFIGI(requestPrice=True) 4208 4209 # Widen raw data with UTC current time (iData["actualDateTime"]): 4210 actualDate = datetime.now(tzutc()) 4211 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4212 4213 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4214 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4215 4216 # Replace some values with human-readable: 4217 iData["nominalCurrency"] = iData["nominal"]["currency"] 4218 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4219 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4220 iData["aciCurrency"] = iData["aciValue"]["currency"] 4221 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4222 iData["issueSize"] = int(iData["issueSize"]) 4223 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4224 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4225 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4226 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4227 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4228 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4229 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4230 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4231 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4232 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4233 4234 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4235 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4236 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4237 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4238 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4239 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4240 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4241 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4242 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4243 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4244 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4245 4246 # Widen raw data with calendar data from `rawCalendar` values: 4247 calendarData = [] 4248 if "events" in iData["rawCalendar"].keys(): 4249 for item in iData["rawCalendar"]["events"]: 4250 calendarData.append({ 4251 "couponDate": item["couponDate"], 4252 "couponNumber": int(item["couponNumber"]), 4253 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4254 "payCurrency": item["payOneBond"]["currency"], 4255 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4256 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4257 "couponStartDate": item["couponStartDate"], 4258 "couponEndDate": item["couponEndDate"], 4259 "couponPeriod": item["couponPeriod"], 4260 }) 4261 4262 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4263 if "maturityDate" not in iData.keys(): 4264 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4265 4266 # Widen raw data with Coupon Rate. 4267 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4268 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4269 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4270 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4271 4272 # Widen raw data with Yield to Maturity (YTM) on current date. 4273 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4274 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4275 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4276 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4277 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4278 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4279 4280 iData["calendar"] = calendarData # adds calendar at the end 4281 4282 # Remove not used data: 4283 iData.pop("uid") 4284 iData.pop("positionUid") 4285 iData.pop("currentPrice") 4286 iData.pop("rawCalendar") 4287 4288 colNames = list(iData.keys()) 4289 if bonds is None: 4290 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4291 4292 else: 4293 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4294 4295 else: 4296 uLogger.warning("Instrument is not a bond!") 4297 4298 processed = round(100 * (i + 1) / iCount, 1) 4299 if tooLong and processed % 5 == 0: 4300 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4301 4302 else: 4303 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4304 4305 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4306 4307 # Saving bonds from Pandas DataFrame to XLSX sheet: 4308 if xlsx and self.bondsXLSXFile: 4309 with pd.ExcelWriter( 4310 path=self.bondsXLSXFile, 4311 date_format=TKS_DATE_FORMAT, 4312 datetime_format=TKS_DATE_TIME_FORMAT, 4313 mode="w", 4314 ) as writer: 4315 bonds.to_excel( 4316 writer, 4317 sheet_name="Extended bonds data", 4318 index=True, 4319 encoding="UTF-8", 4320 freeze_panes=(1, 1), 4321 ) # saving as XLSX-file with freeze first row and column as headers 4322 4323 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4324 4325 return bonds 4326 4327 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4328 """ 4329 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4330 4331 WARNING! This is too long operation if a lot of bonds requested from broker server. 4332 4333 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4334 4335 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4336 extended information about bonds: main info, current prices, bond payment calendar, 4337 coupon yields, current yields and some statistics etc. 4338 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4339 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4340 for further used by data scientists or stock analytics. 4341 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4342 """ 4343 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4344 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4345 4346 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4347 4348 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4349 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4350 calendar = None 4351 for bond in extBonds.iterrows(): 4352 for item in bond[1]["calendar"]: 4353 cData = { 4354 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4355 "couponDate": item["couponDate"], 4356 "figi": bond[1]["figi"], 4357 "ticker": bond[1]["ticker"], 4358 "name": bond[1]["name"], 4359 "couponNumber": item["couponNumber"], 4360 "payOneBond": item["payOneBond"], 4361 "payCurrency": item["payCurrency"], 4362 "couponType": item["couponType"], 4363 "couponPeriod": item["couponPeriod"], 4364 "fixDate": item["fixDate"], 4365 "couponStartDate": item["couponStartDate"], 4366 "couponEndDate": item["couponEndDate"], 4367 } 4368 4369 if calendar is None: 4370 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4371 4372 else: 4373 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4374 4375 if calendar is not None: 4376 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4377 4378 # Saving calendar from Pandas DataFrame to XLSX sheet: 4379 if xlsx: 4380 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4381 4382 with pd.ExcelWriter( 4383 path=xlsxCalendarFile, 4384 date_format=TKS_DATE_FORMAT, 4385 datetime_format=TKS_DATE_TIME_FORMAT, 4386 mode="w", 4387 ) as writer: 4388 humanReadable = calendar.copy(deep=True) 4389 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4390 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4391 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4392 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4393 humanReadable.columns = colNames # human-readable column names 4394 4395 humanReadable.to_excel( 4396 writer, 4397 sheet_name="Bond payments calendar", 4398 index=False, 4399 encoding="UTF-8", 4400 freeze_panes=(1, 2), 4401 ) # saving as XLSX-file with freeze first row and column as headers 4402 4403 del humanReadable # release df in memory 4404 4405 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4406 4407 return calendar 4408 4409 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4410 """ 4411 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4412 Also, creates Markdown file with calendar data, `calendar.md` by default. 4413 4414 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4415 4416 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4417 extended information about bonds: main info, current prices, bond payment calendar, 4418 coupon yields, current yields and some statistics etc. 4419 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4420 :param show: if `True` then also printing bonds payment calendar to the console, 4421 otherwise save to file `calendarFile` only. `False` by default. 4422 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4423 :return: multilines text in Markdown format with bonds payment calendar as a table. 4424 """ 4425 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4426 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4427 4428 infoText = "# Bond payments calendar\n\n" 4429 4430 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4431 4432 if not (calendar is None or calendar.empty): 4433 splitLine = "| | | | | | | | | |\n" 4434 4435 info = [ 4436 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4437 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4438 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4439 ] 4440 4441 newMonth = False 4442 notOneBond = calendar["figi"].nunique() > 1 4443 for i, bond in enumerate(calendar.iterrows()): 4444 if newMonth and notOneBond: 4445 info.append(splitLine) 4446 4447 info.append( 4448 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4449 " √" if bond[1]["paid"] else " —", 4450 bond[1]["couponDate"].split("T")[0], 4451 bond[1]["figi"], 4452 bond[1]["ticker"], 4453 bond[1]["couponNumber"], 4454 "{} {}".format( 4455 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4456 bond[1]["payCurrency"], 4457 ), 4458 bond[1]["couponType"], 4459 bond[1]["couponPeriod"], 4460 bond[1]["fixDate"].split("T")[0], 4461 ) 4462 ) 4463 4464 if i < len(calendar.values) - 1: 4465 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4466 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4467 newMonth = False if curDate.month == nextDate.month else True 4468 4469 else: 4470 newMonth = False 4471 4472 infoText += "".join(info) 4473 4474 if show and not onlyFiles: 4475 uLogger.info("{}".format(infoText)) 4476 4477 if self.calendarFile is not None and (show or onlyFiles): 4478 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4479 fH.write(infoText) 4480 4481 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4482 4483 if self.useHTMLReports: 4484 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4485 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4486 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4487 4488 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4489 4490 else: 4491 infoText += "No data\n" 4492 4493 return infoText 4494 4495 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4496 """ 4497 Method for parsing and show simple table with all available user accounts. 4498 4499 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4500 4501 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4502 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4503 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4504 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4505 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4506 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4507 "closed": "—", "access": "Full access" }, ...}}` 4508 """ 4509 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4510 4511 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4512 accounts = { 4513 item["id"]: { 4514 "type": TKS_ACCOUNT_TYPES[item["type"]], 4515 "name": item["name"], 4516 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4517 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4518 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4519 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4520 } for item in rawAccounts["accounts"] 4521 } 4522 4523 # Raw and parsed data with some fields replaced in "stat" section: 4524 view = { 4525 "rawAccounts": rawAccounts, 4526 "stat": accounts, 4527 } 4528 4529 # --- Prepare simple text table with only accounts data in human-readable format: 4530 if show or onlyFiles: 4531 info = [ 4532 "# User accounts\n\n", 4533 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4534 "| Account ID | Type | Status | Name |\n", 4535 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4536 ] 4537 4538 for account in view["stat"].keys(): 4539 info.extend([ 4540 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4541 account, 4542 view["stat"][account]["type"], 4543 view["stat"][account]["status"], 4544 view["stat"][account]["name"], 4545 ) 4546 ]) 4547 4548 infoText = "".join(info) 4549 4550 if show and not onlyFiles: 4551 uLogger.info(infoText) 4552 4553 if self.userAccountsFile and (show or onlyFiles): 4554 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4555 fH.write(infoText) 4556 4557 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4558 4559 if self.useHTMLReports: 4560 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4561 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4562 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4563 4564 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4565 4566 return view 4567 4568 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4569 """ 4570 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4571 4572 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4573 4574 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4575 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4576 :return: dict with raw parsed data from server and some calculated statistics about it. 4577 """ 4578 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4579 tmpTicker = self._ticker 4580 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4581 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4582 self._ticker = tmpTicker 4583 4584 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4585 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4586 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4587 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4588 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4589 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4590 4591 # This is dict with parsed common user data: 4592 userInfo = { 4593 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4594 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4595 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4596 "tariff": rawUserInfo["tariff"], 4597 } 4598 4599 # This is an array of dict with parsed margin statuses for every account IDs: 4600 margins = {} 4601 for accountId in accounts.keys(): 4602 if rawMargins[accountId]: 4603 margins[accountId] = { 4604 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4605 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4606 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4607 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4608 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4609 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4610 "missing": missing["volume"], 4611 } 4612 4613 else: 4614 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4615 4616 unary = {} # unary-connection limits 4617 for item in rawTariffLimits["unaryLimits"]: 4618 if item["limitPerMinute"] in unary.keys(): 4619 unary[item["limitPerMinute"]].extend(item["methods"]) 4620 4621 else: 4622 unary[item["limitPerMinute"]] = item["methods"] 4623 4624 stream = {} # stream-connection limits 4625 for item in rawTariffLimits["streamLimits"]: 4626 if item["limit"] in stream.keys(): 4627 stream[item["limit"]].extend(item["streams"]) 4628 4629 else: 4630 stream[item["limit"]] = item["streams"] 4631 4632 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4633 limits = { 4634 "unary": unary, 4635 "stream": stream, 4636 } 4637 4638 # Raw and parsed data as an output result: 4639 view = { 4640 "rawUserInfo": rawUserInfo, 4641 "rawAccounts": rawAccounts, 4642 "rawMargins": rawMargins, 4643 "rawTariffLimits": rawTariffLimits, 4644 "stat": { 4645 "overview": overview, 4646 "userInfo": userInfo, 4647 "accounts": accounts, 4648 "margins": margins, 4649 "limits": limits, 4650 }, 4651 } 4652 4653 # --- Prepare text table with user information in human-readable format: 4654 if show or onlyFiles: 4655 info = [ 4656 "# Full user information\n\n", 4657 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4658 "## Common information\n\n", 4659 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4660 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4661 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4662 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4663 "\n## User accounts\n\n", 4664 ] 4665 4666 for account in view["stat"]["accounts"].keys(): 4667 info.extend([ 4668 "### ID: [{}]\n\n".format(account), 4669 "| Parameters | Values |\n", 4670 "|----------------------|--------------------------------------------------------------|\n", 4671 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4672 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4673 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4674 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4675 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4676 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4677 ]) 4678 4679 if margins[account]: 4680 info.extend([ 4681 "| Margin status: | Enabled |\n", 4682 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4683 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4684 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4685 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4686 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4687 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4688 ]) 4689 4690 else: 4691 info.append("| Margin status: | Disabled |\n\n") 4692 4693 info.extend([ 4694 "\n## Current user tariff limits\n", 4695 "\n### See also\n", 4696 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4697 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4698 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4699 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4700 "\n### Unary limits\n", 4701 ]) 4702 4703 if unary: 4704 for key, values in sorted(unary.items()): 4705 info.append("\n* Max requests per minute: {}\n".format(key)) 4706 4707 for value in values: 4708 info.append(" - {}\n".format(value)) 4709 4710 else: 4711 info.append("\nNot available\n") 4712 4713 info.append("\n### Stream limits\n") 4714 4715 if stream: 4716 for key, values in sorted(stream.items()): 4717 info.append("\n* Max stream connections: {}\n".format(key)) 4718 4719 for value in values: 4720 info.append(" - {}\n".format(value)) 4721 4722 else: 4723 info.append("\nNot available\n") 4724 4725 infoText = "".join(info) 4726 4727 if show and not onlyFiles: 4728 uLogger.info(infoText) 4729 4730 if self.userInfoFile and (show or onlyFiles): 4731 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4732 fH.write(infoText) 4733 4734 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4735 4736 if self.useHTMLReports: 4737 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4738 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4739 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4740 4741 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4742 4743 return view 4744 4745 4746class Args: 4747 """ 4748 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4749 """ 4750 def __init__(self, **kwargs): 4751 self.__dict__.update(kwargs) 4752 4753 def __getattr__(self, item): 4754 return None 4755 4756 4757def ParseArgs(): 4758 """This function get and parse command line keys.""" 4759 parser = ArgumentParser() # command-line string parser 4760 4761 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4762 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4763 4764 # --- options: 4765 4766 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4767 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4768 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4769 4770 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4771 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4772 4773 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4774 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4775 4776 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4777 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4778 4779 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4780 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4781 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4782 4783 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4784 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4785 4786 # --- commands: 4787 4788 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4789 4790 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4791 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4792 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4793 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4794 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4795 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4796 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4797 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4798 4799 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4800 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4801 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4802 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4803 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4804 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4805 4806 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4807 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4808 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4809 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4810 4811 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4812 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4813 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4814 4815 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4816 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4817 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4818 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4819 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4820 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4821 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4822 4823 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4824 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4825 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4826 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4827 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4828 4829 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4830 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4831 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4832 4833 cmdArgs = parser.parse_args() 4834 return cmdArgs 4835 4836 4837def Main(**kwargs): 4838 """ 4839 Main function for work with TKSBrokerAPI in the console. 4840 4841 See examples: 4842 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4843 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4844 """ 4845 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4846 4847 if args.debug_level: 4848 uLogger.level = 10 # always debug level by default 4849 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4850 4851 exitCode = 0 4852 start = datetime.now(tzutc()) 4853 uLogger.debug("=-" * 50) 4854 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4855 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4856 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4857 )) 4858 4859 # trying to calculate full current version: 4860 buildVersion = __version__ 4861 try: 4862 v = version("tksbrokerapi") 4863 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4864 4865 except Exception: 4866 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4867 4868 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4869 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4870 4871 try: 4872 if args.version: 4873 print("TKSBrokerAPI {}".format(buildVersion)) 4874 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4875 4876 else: 4877 # Init class for trading with Tinkoff Broker: 4878 trader = TinkoffBrokerServer( 4879 token=args.token, 4880 accountId=args.account_id, 4881 useCache=not args.no_cache, 4882 ) 4883 4884 # --- set some options: 4885 4886 if args.more: 4887 trader.moreDebug = True 4888 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4889 4890 if args.html: 4891 trader.useHTMLReports = True 4892 4893 if args.ticker: 4894 ticker = str(args.ticker).upper() # Tickers may be upper case only 4895 4896 if ticker in trader.aliasesKeys: 4897 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4898 4899 else: 4900 trader.ticker = ticker 4901 4902 if args.figi: 4903 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4904 4905 if args.depth is not None: 4906 trader.depth = args.depth 4907 4908 # --- do one command: 4909 4910 if args.list: 4911 if args.output is not None: 4912 trader.instrumentsFile = args.output 4913 4914 trader.ShowInstrumentsInfo(show=True) 4915 4916 elif args.list_xlsx: 4917 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4918 4919 elif args.bonds_xlsx is not None: 4920 if args.output is not None: 4921 trader.bondsXLSXFile = args.output 4922 4923 if len(args.bonds_xlsx) == 0: 4924 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4925 4926 else: 4927 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4928 4929 elif args.search: 4930 if args.output is not None: 4931 trader.searchResultsFile = args.output 4932 4933 trader.SearchInstruments(pattern=args.search[0], show=True) 4934 4935 elif args.info: 4936 if not (args.ticker or args.figi): 4937 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4938 raise Exception("Ticker or FIGI required") 4939 4940 if args.output is not None: 4941 trader.infoFile = args.output 4942 4943 if args.ticker: 4944 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4945 4946 else: 4947 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4948 4949 elif args.calendar is not None: 4950 if args.output is not None: 4951 trader.calendarFile = args.output 4952 4953 if len(args.calendar) == 0: 4954 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4955 4956 else: 4957 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4958 4959 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4960 4961 elif args.price: 4962 if not (args.ticker or args.figi): 4963 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4964 raise Exception("Ticker or FIGI required") 4965 4966 trader.GetCurrentPrices(show=True) 4967 4968 elif args.prices is not None: 4969 if args.output is not None: 4970 trader.pricesFile = args.output 4971 4972 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4973 4974 elif args.overview: 4975 if args.output is not None: 4976 trader.overviewFile = args.output 4977 4978 trader.Overview(show=True, details="full") 4979 4980 elif args.overview_digest: 4981 if args.output is not None: 4982 trader.overviewDigestFile = args.output 4983 4984 trader.Overview(show=True, details="digest") 4985 4986 elif args.overview_positions: 4987 if args.output is not None: 4988 trader.overviewPositionsFile = args.output 4989 4990 trader.Overview(show=True, details="positions") 4991 4992 elif args.overview_orders: 4993 if args.output is not None: 4994 trader.overviewOrdersFile = args.output 4995 4996 trader.Overview(show=True, details="orders") 4997 4998 elif args.overview_analytics: 4999 if args.output is not None: 5000 trader.overviewAnalyticsFile = args.output 5001 5002 trader.Overview(show=True, details="analytics") 5003 5004 elif args.overview_calendar: 5005 if args.output is not None: 5006 trader.overviewAnalyticsFile = args.output 5007 5008 trader.Overview(show=True, details="calendar") 5009 5010 elif args.deals is not None: 5011 if args.output is not None: 5012 trader.reportFile = args.output 5013 5014 if 0 <= len(args.deals) < 3: 5015 trader.Deals( 5016 start=args.deals[0] if len(args.deals) >= 1 else None, 5017 end=args.deals[1] if len(args.deals) == 2 else None, 5018 show=True, # Always show deals report in console 5019 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5020 ) 5021 5022 else: 5023 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5024 raise Exception("Incorrect value") 5025 5026 elif args.history is not None: 5027 if args.output is not None: 5028 trader.historyFile = args.output 5029 5030 if 0 <= len(args.history) < 3: 5031 dataReceived = trader.History( 5032 start=args.history[0] if len(args.history) >= 1 else None, 5033 end=args.history[1] if len(args.history) == 2 else None, 5034 interval="hour" if args.interval is None or not args.interval else args.interval, 5035 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5036 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5037 show=True, # shows all downloaded candles in console 5038 ) 5039 5040 if args.render_chart is not None and dataReceived is not None: 5041 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5042 5043 trader.ShowHistoryChart( 5044 candles=dataReceived, 5045 interact=iChart, 5046 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5047 ) 5048 5049 else: 5050 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5051 raise Exception("Incorrect value") 5052 5053 elif args.load_history is not None: 5054 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5055 5056 if args.render_chart is not None and histData is not None: 5057 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5058 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5059 5060 trader.ShowHistoryChart( 5061 candles=histData, 5062 interact=iChart, 5063 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5064 ) 5065 5066 elif args.trade is not None: 5067 if 1 <= len(args.trade) <= 5: 5068 trader.Trade( 5069 operation=args.trade[0], 5070 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5071 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5072 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5073 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5074 ) 5075 5076 else: 5077 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5078 5079 elif args.buy is not None: 5080 if 0 <= len(args.buy) <= 4: 5081 trader.Buy( 5082 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5083 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5084 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5085 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5086 ) 5087 5088 else: 5089 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5090 5091 elif args.sell is not None: 5092 if 0 <= len(args.sell) <= 4: 5093 trader.Sell( 5094 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5095 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5096 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5097 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5098 ) 5099 5100 else: 5101 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5102 5103 elif args.order: 5104 if 4 <= len(args.order) <= 7: 5105 trader.Order( 5106 operation=args.order[0], 5107 orderType=args.order[1], 5108 lots=int(args.order[2]), 5109 targetPrice=float(args.order[3]), 5110 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5111 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5112 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5113 ) 5114 5115 else: 5116 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5117 5118 elif args.buy_limit: 5119 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5120 5121 elif args.sell_limit: 5122 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5123 5124 elif args.buy_stop: 5125 if 2 <= len(args.buy_stop) <= 7: 5126 trader.BuyStop( 5127 lots=int(args.buy_stop[0]), 5128 targetPrice=float(args.buy_stop[1]), 5129 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5130 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5131 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5132 ) 5133 5134 else: 5135 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5136 5137 elif args.sell_stop: 5138 if 2 <= len(args.sell_stop) <= 7: 5139 trader.SellStop( 5140 lots=int(args.sell_stop[0]), 5141 targetPrice=float(args.sell_stop[1]), 5142 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5143 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5144 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5145 ) 5146 5147 else: 5148 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5149 5150 # elif args.buy_order_grid is not None: 5151 # # update order grid work with api v2 5152 # if len(args.buy_order_grid) == 2: 5153 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5154 # 5155 # for order in orderParams: 5156 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5157 # 5158 # else: 5159 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5160 # 5161 # elif args.sell_order_grid is not None: 5162 # # update order grid work with api v2 5163 # if len(args.sell_order_grid) >= 2: 5164 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5165 # 5166 # for order in orderParams: 5167 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5168 # 5169 # else: 5170 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5171 5172 elif args.close_order is not None: 5173 trader.CloseOrders(args.close_order) # close only one order 5174 5175 elif args.close_orders is not None: 5176 trader.CloseOrders(args.close_orders) # close list of orders 5177 5178 elif args.close_trade: 5179 if not (args.ticker or args.figi): 5180 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5181 raise Exception("Ticker or FIGI required") 5182 5183 if args.ticker: 5184 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5185 5186 else: 5187 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5188 5189 elif args.close_trades is not None: 5190 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5191 5192 elif args.close_all is not None: 5193 if args.ticker: 5194 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5195 5196 elif args.figi: 5197 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5198 5199 else: 5200 trader.CloseAll(*args.close_all) 5201 5202 elif args.limits: 5203 if args.output is not None: 5204 trader.withdrawalLimitsFile = args.output 5205 5206 trader.OverviewLimits(show=True) 5207 5208 elif args.user_info: 5209 if args.output is not None: 5210 trader.userInfoFile = args.output 5211 5212 trader.OverviewUserInfo(show=True) 5213 5214 elif args.account: 5215 if args.output is not None: 5216 trader.userAccountsFile = args.output 5217 5218 trader.OverviewAccounts(show=True) 5219 5220 else: 5221 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5222 raise Exception("There is no command to execute") 5223 5224 except Exception: 5225 trace = tb.format_exc() 5226 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5227 if e in trace: 5228 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5229 break 5230 5231 uLogger.debug(trace) 5232 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5233 exitCode = 255 # an error occurred, must be open a ticket for this issue 5234 5235 finally: 5236 finish = datetime.now(tzutc()) 5237 5238 if exitCode == 0: 5239 if args.more: 5240 uLogger.debug("All operations were finished success (summary code is 0).") 5241 5242 else: 5243 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5244 os.path.abspath(uLog.defaultLogFile), exitCode, 5245 )) 5246 5247 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5248 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5249 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5250 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5251 )) 5252 uLogger.debug("=-" * 50) 5253 5254 if not kwargs: 5255 sys.exit(exitCode) 5256 5257 else: 5258 return exitCode 5259 5260 5261if __name__ == "__main__": 5262 Main()
78class TinkoffBrokerServer: 79 """ 80 This class implements methods to work with Tinkoff broker server. 81 82 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 83 84 About `token`: https://tinkoff.github.io/investAPI/token/ 85 """ 86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.__lock = Lock() # initialize multiprocessing mutex lock 130 131 self.aliases = TKS_TICKER_ALIASES 132 """Some aliases instead official tickers. 133 134 See also: `TKSEnums.TKS_TICKER_ALIASES` 135 """ 136 137 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 138 139 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 140 141 self._ticker = "" 142 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 143 144 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 145 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 146 147 See also: `SearchByTicker()`, `SearchInstruments()`. 148 """ 149 150 self._figi = "" 151 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 152 153 See also: `SearchByFIGI()`, `SearchInstruments()`. 154 """ 155 156 self.depth = 1 157 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 158 159 See also: `GetCurrentPrices()`. 160 """ 161 162 self.server = r"https://invest-public-api.tinkoff.ru/rest" 163 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 164 165 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 166 """ 167 168 uLogger.debug("Broker API server: {}".format(self.server)) 169 170 self.timeout = 15 171 """Server operations timeout in seconds. Default: `15`. 172 173 See also: `SendAPIRequest()`. 174 """ 175 176 self.headers = { 177 "Content-Type": "application/json", 178 "accept": "application/json", 179 "Authorization": "Bearer {}".format(self.token), 180 "x-app-name": "Tim55667757.TKSBrokerAPI", 181 } 182 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 183 184 See also: `SendAPIRequest()`. 185 """ 186 187 self.body = None 188 """Request body which send to broker server. Default: `None`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.moreDebug = False 194 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 195 196 self.useHTMLReports = False 197 """ 198 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 199 200 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 201 """ 202 203 self.historyFile = None 204 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 205 206 See also: `History()`. 207 """ 208 209 self.htmlHistoryFile = "index.html" 210 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 211 212 See also: `ShowHistoryChart()`. 213 """ 214 215 self.instrumentsFile = "instruments.md" 216 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 217 218 See also: `ShowInstrumentsInfo()`. 219 """ 220 221 self.searchResultsFile = "search-results.md" 222 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 223 224 See also: `SearchInstruments()`. 225 """ 226 227 self.pricesFile = "prices.md" 228 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 229 230 See also: `GetListOfPrices()`. 231 """ 232 233 self.infoFile = "info.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 237 """ 238 239 self.bondsXLSXFile = "ext-bonds.xlsx" 240 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 241 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 242 243 See also: `ExtendBondsData()`. 244 """ 245 246 self.calendarFile = "calendar.md" 247 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 248 249 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 250 251 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 252 """ 253 254 self.overviewFile = "overview.md" 255 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 256 257 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 258 """ 259 260 self.overviewDigestFile = "overview-digest.md" 261 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 262 263 See also: `Overview()` with parameter `details="digest"`. 264 """ 265 266 self.overviewPositionsFile = "overview-positions.md" 267 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 268 269 See also: `Overview()` with parameter `details="positions"`. 270 """ 271 272 self.overviewOrdersFile = "overview-orders.md" 273 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 274 275 See also: `Overview()` with parameter `details="orders"`. 276 """ 277 278 self.overviewAnalyticsFile = "overview-analytics.md" 279 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 280 281 See also: `Overview()` with parameter `details="analytics"`. 282 """ 283 284 self.overviewBondsCalendarFile = "overview-calendar.md" 285 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 286 287 See also: `Overview()` with parameter `details="calendar"`. 288 """ 289 290 self.reportFile = "deals.md" 291 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 292 293 See also: `Deals()`. 294 """ 295 296 self.withdrawalLimitsFile = "limits.md" 297 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 298 299 See also: `OverviewLimits()` and `RequestLimits()`. 300 """ 301 302 self.userInfoFile = "user-info.md" 303 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 304 305 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 306 """ 307 308 self.userAccountsFile = "accounts.md" 309 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 310 311 See also: `OverviewAccounts()`, `RequestAccounts()`. 312 """ 313 314 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 315 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 316 317 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 318 319 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 320 """ 321 322 self.iList = None # init iList for raw instruments data 323 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 324 325 See also: `Listing()`, `DumpInstruments()`. 326 """ 327 328 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 329 if useCache: 330 if os.path.exists(self.iListDumpFile): 331 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 332 curTime = datetime.now(tzutc()) 333 334 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 335 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 336 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 338 339 else: 340 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 341 342 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 343 os.path.abspath(self.iListDumpFile), 344 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 345 )) 346 347 else: 348 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 349 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 350 351 else: 352 self.iList = self.Listing() # request new raw instruments data from broker server 353 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 354 355 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 356 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 357 358 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 359 """ 360 361 @property 362 def ticker(self) -> str: 363 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 364 365 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 366 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 367 368 See also: `SearchByTicker()`, `SearchInstruments()`. 369 """ 370 return self._ticker 371 372 @ticker.setter 373 def ticker(self, value): 374 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 375 376 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 377 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 378 379 See also: `SearchByTicker()`, `SearchInstruments()`. 380 """ 381 self._ticker = str(value).upper() # Tickers may be upper case only 382 383 @property 384 def figi(self) -> str: 385 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 386 387 See also: `SearchByFIGI()`, `SearchInstruments()`. 388 """ 389 return self._figi 390 391 @figi.setter 392 def figi(self, value): 393 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 394 395 See also: `SearchByFIGI()`, `SearchInstruments()`. 396 """ 397 self._figi = str(value).upper() # FIGI may be upper case only 398 399 def _ParseJSON(self, rawData="{}") -> dict: 400 """ 401 Parse JSON from response string. 402 403 :param rawData: this is a string with JSON-formatted text. 404 :return: JSON (dictionary), parsed from server response string. 405 """ 406 responseJSON = json.loads(rawData) if rawData else {} 407 408 if self.moreDebug: 409 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 410 411 return responseJSON 412 413 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 414 """ 415 Send GET or POST request to broker server and receive JSON object. 416 417 self.header: must be defining with dictionary of headers. 418 self.body: if define then used as request body. None by default. 419 self.timeout: global request timeout, 15 seconds by default. 420 :param url: url with REST request. 421 :param reqType: send "GET" or "POST" request. "GET" by default. 422 :param retry: how many times retry after first request if an 5xx server errors occurred. 423 :param pause: sleep time in seconds between retries. 424 :return: response JSON (dictionary) from broker. 425 """ 426 if reqType.upper() not in ("GET", "POST"): 427 uLogger.error("You can define request type: `GET` or `POST`!") 428 raise Exception("Incorrect value") 429 430 if self.moreDebug: 431 uLogger.debug("Request parameters:") 432 uLogger.debug(" - REST API URL: {}".format(url)) 433 uLogger.debug(" - request type: {}".format(reqType)) 434 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 435 uLogger.debug(" - body:\n{}".format(self.body)) 436 437 # fast hack to avoid all operations with some tickers/FIGI 438 responseJSON = {} 439 oK = True 440 for item in self.exclude: 441 if item in url: 442 if self.moreDebug: 443 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 444 445 oK = False 446 break 447 448 if oK: 449 with self.__lock: # acquire the mutex lock 450 counter = 0 451 response = None 452 errMsg = "" 453 454 while not response and counter <= retry: 455 if reqType == "GET": 456 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 457 458 if reqType == "POST": 459 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 460 461 if self.moreDebug: 462 uLogger.debug("Response:") 463 uLogger.debug(" - status code: {}".format(response.status_code)) 464 uLogger.debug(" - reason: {}".format(response.reason)) 465 uLogger.debug(" - body length: {}".format(len(response.text))) 466 uLogger.debug(" - headers:\n{}".format(response.headers)) 467 468 # Server returns some headers: 469 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 470 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 471 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 472 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 473 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 474 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 475 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 476 sleep(rateLimitWait) 477 478 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 479 if 400 <= response.status_code < 500: 480 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 481 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 482 483 if "code" in response.text and "message" in response.text: 484 msgDict = self._ParseJSON(rawData=response.text) 485 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 486 487 counter = retry + 1 # do not retry for 4xx errors 488 489 if 500 <= response.status_code < 600: 490 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 491 uLogger.debug(" - not oK, {}".format(errMsg)) 492 493 if "code" in response.text and "message" in response.text: 494 errMsgDict = self._ParseJSON(rawData=response.text) 495 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 496 497 counter += 1 498 499 if counter <= retry: 500 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 501 sleep(pause) 502 503 responseJSON = self._ParseJSON(rawData=response.text) 504 505 if errMsg: 506 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 507 uLogger.error(" - not oK, {}".format(errMsg)) 508 509 return responseJSON 510 511 def _IUpdater(self, iType: str) -> tuple: 512 """ 513 Request instrument by type from server. See available API methods for instruments: 514 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 515 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 516 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 517 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 518 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 519 520 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 521 :return: tuple with iType name and list of available instruments of current type for defined user token. 522 """ 523 result = [] 524 525 if iType in TKS_INSTRUMENTS: 526 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 527 528 # all instruments have the same body in API v2 requests: 529 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 530 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 531 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 532 533 return iType, result 534 535 def _IWrapper(self, kwargs): 536 """ 537 Wrapper runs instrument's update method `_IUpdater()`. 538 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 539 """ 540 return self._IUpdater(**kwargs) 541 542 def Listing(self) -> dict: 543 """ 544 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 545 546 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 547 """ 548 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 549 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 550 551 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 552 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 553 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 554 555 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 556 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 557 poolUpdater.close() # close the thread pool 558 poolUpdater.join() # wait a moment until all data returns from threads 559 560 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 561 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 562 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 563 564 # calculate minimum price increment (step) for all instruments and set up instrument's type: 565 for iType in iList.keys(): 566 for ticker in iList[iType]: 567 iList[iType][ticker]["type"] = iType 568 569 if "minPriceIncrement" in iList[iType][ticker].keys(): 570 iList[iType][ticker]["step"] = NanoToFloat( 571 iList[iType][ticker]["minPriceIncrement"]["units"], 572 iList[iType][ticker]["minPriceIncrement"]["nano"], 573 ) 574 575 else: 576 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 577 578 return iList 579 580 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 581 """ 582 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 583 584 See also: `DumpInstruments()`, `Listing()`. 585 586 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 587 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 588 """ 589 if self.iListDumpFile is None or not self.iListDumpFile: 590 uLogger.error("Output name of dump file must be defined!") 591 raise Exception("Filename required") 592 593 if not self.iList or forceUpdate: 594 self.iList = self.Listing() 595 596 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 597 598 # Save as XLSX with separated sheets for every type of instruments: 599 with pd.ExcelWriter( 600 path=xlsxDumpFile, 601 date_format=TKS_DATE_FORMAT, 602 datetime_format=TKS_DATE_TIME_FORMAT, 603 mode="w", 604 ) as writer: 605 for iType in TKS_INSTRUMENTS: 606 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 607 df = df[sorted(df)] # sorted by column names 608 df = df.applymap( 609 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 610 na_action="ignore", 611 ) # converting numbers from nano-type to float in every cell 612 df.to_excel( 613 writer, 614 sheet_name=iType, 615 encoding="UTF-8", 616 freeze_panes=(1, 1), 617 ) # saving as XLSX-file with freeze first row and column as headers 618 619 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 620 621 def DumpInstruments(self, forceUpdate: bool = True) -> str: 622 """ 623 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 624 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 625 626 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 627 628 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 629 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 630 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 631 """ 632 if self.iListDumpFile is None or not self.iListDumpFile: 633 uLogger.error("Output name of dump file must be defined!") 634 raise Exception("Filename required") 635 636 if not self.iList or forceUpdate: 637 self.iList = self.Listing() 638 639 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 640 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 641 fH.write(jsonDump) 642 643 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 644 645 return jsonDump 646 647 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 648 """ 649 Show information about one instrument defined by json data and prints it in Markdown format. 650 651 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 652 653 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 654 :param show: if `True` then also printing information about instrument and its current price. 655 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 656 :return: multilines text in Markdown format with information about one instrument. 657 """ 658 splitLine = "| | |\n" 659 infoText = "" 660 661 if iJSON is not None and iJSON and isinstance(iJSON, dict): 662 info = [ 663 "# Main information\n\n", 664 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 665 "| Parameters | Values |\n", 666 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 667 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 668 "| Full name: | {:<54} |\n".format(iJSON["name"]), 669 ] 670 671 if "sector" in iJSON.keys() and iJSON["sector"]: 672 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 673 674 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 675 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 676 677 info.extend([ 678 splitLine, 679 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 680 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 681 ]) 682 683 if "isin" in iJSON.keys() and iJSON["isin"]: 684 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 685 686 if "classCode" in iJSON.keys(): 687 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 688 689 info.extend([ 690 splitLine, 691 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 692 splitLine, 693 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 694 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 695 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 696 ]) 697 698 if iJSON["figi"]: 699 self._figi = iJSON["figi"] 700 iJSON = iJSON | self.RequestTradingStatus() 701 702 info.extend([ 703 splitLine, 704 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 705 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 706 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 707 ]) 708 709 info.append(splitLine) 710 711 if "type" in iJSON.keys() and iJSON["type"]: 712 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 713 714 if "shareType" in iJSON.keys() and iJSON["shareType"]: 715 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 716 717 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 718 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 719 720 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 721 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 722 723 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 724 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 725 726 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 727 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 728 729 if "focusType" in iJSON.keys() and iJSON["focusType"]: 730 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 731 732 if "assetType" in iJSON.keys() and iJSON["assetType"]: 733 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 734 735 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 736 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 737 738 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 739 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 740 741 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 742 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 743 744 if "currency" in iJSON.keys(): 745 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 746 747 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 748 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 749 750 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 751 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 752 753 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 754 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 755 756 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 757 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 758 759 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 760 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 761 762 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 763 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 764 765 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 766 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 767 768 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 769 info.append("| Perpetual bond: | Yes |\n") 770 771 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 772 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 773 774 iExt = None 775 if iJSON["type"] == "Bonds": 776 info.extend([ 777 splitLine, 778 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 779 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 780 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 781 iJSON["nominal"]["currency"], 782 )), 783 ]) 784 785 if "floatingCouponFlag" in iJSON.keys(): 786 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 787 788 if "amortizationFlag" in iJSON.keys(): 789 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 790 791 info.append(splitLine) 792 793 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 794 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 795 796 if iJSON["figi"]: 797 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 798 799 info.extend([ 800 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 801 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 802 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 803 ]) 804 805 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 806 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 807 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 808 iJSON["aciValue"]["currency"] 809 ))) 810 811 if "currentPrice" in iJSON.keys(): 812 info.append(splitLine) 813 814 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 815 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 816 817 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 818 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 819 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 820 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 821 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 822 823 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 824 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 825 826 info.extend([ 827 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 828 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 829 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 830 )), 831 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 832 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 833 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 834 )), 835 "| Changes between last deal price and last close | {:<54} |\n".format( 836 "{:.2f}%{}".format( 837 iJSON["currentPrice"]["changes"], 838 " ({}{:.2f} {})".format( 839 "+" if bondChangesDelta > 0 else "", 840 bondChangesDelta, 841 aciCurrency 842 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 843 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 844 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 845 currency 846 ), 847 ) 848 ), 849 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 850 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 851 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 852 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 853 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 854 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 855 )), 856 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 857 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 858 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 859 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 860 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 861 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 862 )), 863 ]) 864 865 if "lot" in iJSON.keys(): 866 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 867 868 if "step" in iJSON.keys() and iJSON["step"] != 0: 869 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 870 871 # Add bond payment calendar: 872 if iJSON["type"] == "Bonds": 873 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 874 info.extend(["\n#", strCalendar]) 875 876 infoText += "".join(info) 877 878 if show and not onlyFiles: 879 uLogger.info("{}".format(infoText)) 880 881 if self.infoFile is not None and (show or onlyFiles): 882 with open(self.infoFile, "w", encoding="UTF-8") as fH: 883 fH.write(infoText) 884 885 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 886 887 if self.useHTMLReports: 888 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 889 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 890 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 891 892 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 893 894 return infoText 895 896 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 897 """ 898 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 899 900 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 901 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 902 :return: JSON formatted data with information about instrument. 903 """ 904 tickerJSON = {} 905 if self.moreDebug: 906 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 907 908 if not self._ticker: 909 uLogger.warning("self._ticker variable is not be empty!") 910 911 else: 912 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 913 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 914 raise Exception("Instrument not allowed") 915 916 if not self.iList: 917 self.iList = self.Listing() 918 919 if self._ticker in self.iList["Shares"].keys(): 920 tickerJSON = self.iList["Shares"][self._ticker] 921 if self.moreDebug: 922 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 923 924 elif self._ticker in self.iList["Currencies"].keys(): 925 tickerJSON = self.iList["Currencies"][self._ticker] 926 if self.moreDebug: 927 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 928 929 elif self._ticker in self.iList["Bonds"].keys(): 930 tickerJSON = self.iList["Bonds"][self._ticker] 931 if self.moreDebug: 932 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 933 934 elif self._ticker in self.iList["Etfs"].keys(): 935 tickerJSON = self.iList["Etfs"][self._ticker] 936 if self.moreDebug: 937 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 938 939 elif self._ticker in self.iList["Futures"].keys(): 940 tickerJSON = self.iList["Futures"][self._ticker] 941 if self.moreDebug: 942 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 943 944 if tickerJSON: 945 self._figi = tickerJSON["figi"] 946 947 if requestPrice: 948 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 949 950 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 951 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 952 953 else: 954 tickerJSON["currentPrice"]["changes"] = 0 955 956 if show: 957 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 958 959 else: 960 if show: 961 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 962 963 return tickerJSON 964 965 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 966 """ 967 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 968 969 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 970 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 971 :return: JSON formatted data with information about instrument. 972 """ 973 figiJSON = {} 974 if self.moreDebug: 975 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 976 977 if not self._figi: 978 uLogger.warning("self._figi variable is not be empty!") 979 980 else: 981 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 982 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 983 raise Exception("Instrument not allowed") 984 985 if not self.iList: 986 self.iList = self.Listing() 987 988 for item in self.iList["Shares"].keys(): 989 if self._figi == self.iList["Shares"][item]["figi"]: 990 figiJSON = self.iList["Shares"][item] 991 992 if self.moreDebug: 993 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 994 995 break 996 997 if not figiJSON: 998 for item in self.iList["Currencies"].keys(): 999 if self._figi == self.iList["Currencies"][item]["figi"]: 1000 figiJSON = self.iList["Currencies"][item] 1001 1002 if self.moreDebug: 1003 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1004 1005 break 1006 1007 if not figiJSON: 1008 for item in self.iList["Bonds"].keys(): 1009 if self._figi == self.iList["Bonds"][item]["figi"]: 1010 figiJSON = self.iList["Bonds"][item] 1011 1012 if self.moreDebug: 1013 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1014 1015 break 1016 1017 if not figiJSON: 1018 for item in self.iList["Etfs"].keys(): 1019 if self._figi == self.iList["Etfs"][item]["figi"]: 1020 figiJSON = self.iList["Etfs"][item] 1021 1022 if self.moreDebug: 1023 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1024 1025 break 1026 1027 if not figiJSON: 1028 for item in self.iList["Futures"].keys(): 1029 if self._figi == self.iList["Futures"][item]["figi"]: 1030 figiJSON = self.iList["Futures"][item] 1031 1032 if self.moreDebug: 1033 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1034 1035 break 1036 1037 if figiJSON: 1038 self._figi = figiJSON["figi"] 1039 self._ticker = figiJSON["ticker"] 1040 1041 if requestPrice: 1042 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1043 1044 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1045 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1046 1047 else: 1048 figiJSON["currentPrice"]["changes"] = 0 1049 1050 if show: 1051 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1052 1053 else: 1054 if show: 1055 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1056 1057 return figiJSON 1058 1059 def GetCurrentPrices(self, show: bool = True) -> dict: 1060 """ 1061 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1062 `{"buy": [{"price": 1243.8, "quantity": 193}, 1063 {"price": 1244.0, "quantity": 168}, 1064 {"price": 1244.8, "quantity": 5}, 1065 {"price": 1245.0, "quantity": 61}, 1066 {"price": 1245.4, "quantity": 60}], 1067 "sell": [{"price": 1243.6, "quantity": 8}, 1068 {"price": 1242.6, "quantity": 10}, 1069 {"price": 1242.4, "quantity": 18}, 1070 {"price": 1242.2, "quantity": 50}, 1071 {"price": 1242.0, "quantity": 113}], 1072 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1073 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1074 - sell: list of dicts with Buyers prices, 1075 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1076 - quantity: volume value by current price in lots, 1077 - limitUp: current trade session limit price, maximum, 1078 - limitDown: current trade session limit price, minimum, 1079 - lastPrice: last deal price of the instrument, 1080 - closePrice: previous trade session close price of the instrument. 1081 1082 See also: `SearchByTicker()` and `SearchByFIGI()`. 1083 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1084 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1085 1086 :param show: if `True` then print DOM to log and console. 1087 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1088 If an error occurred then returns an empty record: 1089 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1090 """ 1091 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1092 1093 if self.depth < 1: 1094 uLogger.error("Depth of Market (DOM) must be >=1!") 1095 raise Exception("Incorrect value") 1096 1097 if not (self._ticker or self._figi): 1098 uLogger.error("self._ticker or self._figi variables must be defined!") 1099 raise Exception("Ticker or FIGI required") 1100 1101 if self._ticker and not self._figi: 1102 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1103 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1104 1105 if not self._ticker and self._figi: 1106 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1107 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1108 1109 if not self._figi: 1110 uLogger.error("FIGI is not defined!") 1111 raise Exception("Ticker or FIGI required") 1112 1113 else: 1114 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1115 1116 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1117 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1118 self.body = str({"figi": self._figi, "depth": self.depth}) 1119 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1120 1121 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1122 # list of dicts with sellers orders: 1123 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1124 1125 # list of dicts with buyers orders: 1126 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1127 1128 # max price of instrument at this time: 1129 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1130 1131 # min price of instrument at this time: 1132 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1133 1134 # last price of deal with instrument: 1135 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1136 1137 # last close price of instrument: 1138 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1139 1140 else: 1141 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1142 uLogger.debug("Server response: {}".format(pricesResponse)) 1143 1144 if show: 1145 if prices["buy"] or prices["sell"]: 1146 info = [ 1147 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1148 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1149 self._ticker, 1150 self._figi, 1151 self.depth, 1152 ), 1153 "-" * 60, "\n", 1154 " Orders of Buyers | Orders of Sellers\n", 1155 "-" * 60, "\n", 1156 " Sell prices (volumes) | Buy prices (volumes)\n", 1157 "-" * 60, "\n", 1158 ] 1159 1160 if not prices["buy"]: 1161 info.append(" | No orders!\n") 1162 sumBuy = 0 1163 1164 else: 1165 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1166 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1167 for item in maxMinSorted: 1168 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1169 1170 if not prices["sell"]: 1171 info.append("No orders! |\n") 1172 sumSell = 0 1173 1174 else: 1175 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1176 for item in prices["sell"]: 1177 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1178 1179 info.extend([ 1180 "-" * 60, "\n", 1181 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1182 "-" * 60, "\n", 1183 ]) 1184 1185 infoText = "".join(info) 1186 1187 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1188 1189 else: 1190 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1191 1192 return prices 1193 1194 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1195 """ 1196 This method get and show information about all available broker instruments for current user account. 1197 If `instrumentsFile` string is not empty then also save information to this file. 1198 1199 :param show: if `True` then print results to console, if `False` — print only to file. 1200 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1201 :return: multi-lines string with all available broker instruments. 1202 """ 1203 if not self.iList: 1204 self.iList = self.Listing() 1205 1206 info = [ 1207 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1208 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1209 ] 1210 1211 # add instruments count by type: 1212 for iType in self.iList.keys(): 1213 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1214 1215 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1216 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1217 1218 # generating info tables with all instruments by type: 1219 for iType in self.iList.keys(): 1220 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1221 1222 for instrument in self.iList[iType].keys(): 1223 iName = self.iList[iType][instrument]["name"] # instrument's name 1224 if len(iName) > 57: 1225 iName = "{}...".format(iName[:54]) # right trim for a long string 1226 1227 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1228 self.iList[iType][instrument]["ticker"], 1229 iName, 1230 self.iList[iType][instrument]["figi"], 1231 self.iList[iType][instrument]["currency"], 1232 self.iList[iType][instrument]["lot"], 1233 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1234 )) 1235 1236 infoText = "".join(info) 1237 1238 if show and not onlyFiles: 1239 uLogger.info(infoText) 1240 1241 if self.instrumentsFile and (show or onlyFiles): 1242 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1243 fH.write(infoText) 1244 1245 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1246 1247 if self.useHTMLReports: 1248 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1249 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1250 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1251 1252 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1253 1254 return infoText 1255 1256 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1257 """ 1258 This method search and show information about instruments by part of its ticker, FIGI or name. 1259 If `searchResultsFile` string is not empty then also save information to this file. 1260 1261 :param pattern: string with part of ticker, FIGI or instrument's name. 1262 :param show: if `True` then print results to console, if `False` — return list of result only. 1263 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1264 :return: list of dictionaries with all found instruments. 1265 """ 1266 if not self.iList: 1267 self.iList = self.Listing() 1268 1269 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1270 compiledPattern = re.compile(pattern, re.IGNORECASE) 1271 1272 for iType in self.iList: 1273 for instrument in self.iList[iType].values(): 1274 searchResult = compiledPattern.search(" ".join( 1275 [instrument["ticker"], instrument["figi"], instrument["name"]] 1276 )) 1277 1278 if searchResult: 1279 searchResults[iType][instrument["ticker"]] = instrument 1280 1281 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1282 info = [ 1283 "# Search results\n\n", 1284 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1285 "* **Search pattern:** [{}]\n".format(pattern), 1286 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1287 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1288 ] 1289 infoShort = info[:] 1290 1291 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1292 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1293 skippedLine = "| ... | ... | ... | ... |\n" 1294 1295 if resultsLen == 0: 1296 info.append("\nNo results\n") 1297 infoShort.append("\nNo results\n") 1298 uLogger.warning("No results. Try changing your search pattern.") 1299 1300 else: 1301 for iType in searchResults: 1302 iTypeValuesCount = len(searchResults[iType].values()) 1303 if iTypeValuesCount > 0: 1304 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1305 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1306 1307 for instrument in searchResults[iType].values(): 1308 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1309 instrument["type"], 1310 instrument["ticker"], 1311 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1312 instrument["figi"], 1313 )) 1314 1315 if iTypeValuesCount <= 5: 1316 infoShort.extend(info[-iTypeValuesCount:]) 1317 1318 else: 1319 infoShort.extend(info[-5:]) 1320 infoShort.append(skippedLine) 1321 1322 infoText = "".join(info) 1323 infoTextShort = "".join(infoShort) 1324 1325 if show and not onlyFiles: 1326 uLogger.info(infoTextShort) 1327 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1328 1329 if self.searchResultsFile and (show or onlyFiles): 1330 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1331 fH.write(infoText) 1332 1333 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1334 1335 if self.useHTMLReports: 1336 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1337 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1338 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1339 1340 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1341 1342 return searchResults 1343 1344 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1345 """ 1346 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1347 1348 :param instruments: list of strings with tickers or FIGIs. 1349 :return: list with unique instrument FIGIs only. 1350 """ 1351 requestedInstruments = [] 1352 for iName in instruments: 1353 if iName not in self.aliases.keys(): 1354 if iName not in requestedInstruments: 1355 requestedInstruments.append(iName) 1356 1357 else: 1358 if iName not in requestedInstruments: 1359 if self.aliases[iName] not in requestedInstruments: 1360 requestedInstruments.append(self.aliases[iName]) 1361 1362 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1363 1364 onlyUniqueFIGIs = [] 1365 for iName in requestedInstruments: 1366 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1367 continue 1368 1369 self._ticker = iName 1370 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1371 1372 if not iData: 1373 self._ticker = "" 1374 self._figi = iName 1375 1376 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1377 1378 if not iData: 1379 self._figi = "" 1380 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1381 1382 if iData and iData["figi"] not in onlyUniqueFIGIs: 1383 onlyUniqueFIGIs.append(iData["figi"]) 1384 1385 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1386 1387 return onlyUniqueFIGIs 1388 1389 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1390 """ 1391 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1392 1393 See limits: https://tinkoff.github.io/investAPI/limits/ 1394 1395 If `pricesFile` string is not empty then also save information to this file. 1396 1397 :param instruments: list of strings with tickers or FIGIs. 1398 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1399 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1400 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1401 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1402 """ 1403 if instruments is None or not instruments: 1404 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1405 raise Exception("Ticker or FIGI required") 1406 1407 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1408 1409 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1410 1411 iList = [] # trying to get info and current prices about all unique instruments: 1412 for self._figi in onlyUniqueFIGIs: 1413 iData = self.SearchByFIGI(requestPrice=True, show=False) 1414 iList.append(iData) 1415 1416 self.ShowListOfPrices(iList, show, onlyFiles) 1417 1418 return iList 1419 1420 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1421 """ 1422 Show table contains current prices of given instruments. 1423 1424 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1425 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1426 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1427 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1428 :return: multilines text in Markdown format as a table contains current prices. 1429 """ 1430 infoText = "" 1431 1432 if show or self.pricesFile or onlyFiles: 1433 info = [ 1434 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1435 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1436 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1437 ] 1438 1439 for item in iList: 1440 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1441 item["ticker"], 1442 item["figi"], 1443 item["type"], 1444 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1445 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1446 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1447 "{} / {}".format( 1448 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1449 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1450 ), 1451 "{} / {}".format( 1452 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1453 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1454 ), 1455 item["currency"], 1456 )) 1457 1458 infoText = "".join(info) 1459 1460 if show and not onlyFiles: 1461 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1462 1463 if self.pricesFile and (show or onlyFiles): 1464 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1465 fH.write(infoText) 1466 1467 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1468 1469 if self.useHTMLReports: 1470 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1471 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1472 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1473 1474 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1475 1476 return infoText 1477 1478 def RequestTradingStatus(self) -> dict: 1479 """ 1480 Requesting trading status for the instrument defined by `figi` variable. 1481 1482 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1483 1484 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1485 1486 :return: dictionary with trading status attributes. Response example: 1487 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1488 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1489 """ 1490 if self._figi is None or not self._figi: 1491 uLogger.error("Variable `figi` must be defined for using this method!") 1492 raise Exception("FIGI required") 1493 1494 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1495 1496 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1497 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1498 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1499 1500 if self.moreDebug: 1501 uLogger.debug("Records about current trading status successfully received") 1502 1503 return tradingStatus 1504 1505 def RequestPortfolio(self) -> dict: 1506 """ 1507 Requesting actual user's portfolio for current `accountId`. 1508 1509 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1510 1511 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1512 1513 :return: dictionary with user's portfolio. 1514 """ 1515 if self.accountId is None or not self.accountId: 1516 uLogger.error("Variable `accountId` must be defined for using this method!") 1517 raise Exception("Account ID required") 1518 1519 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1520 1521 self.body = str({"accountId": self.accountId}) 1522 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1523 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1524 1525 if self.moreDebug: 1526 uLogger.debug("Records about user's portfolio successfully received") 1527 1528 return rawPortfolio 1529 1530 def RequestPositions(self) -> dict: 1531 """ 1532 Requesting open positions by currencies and instruments for current `accountId`. 1533 1534 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1535 1536 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1537 1538 :return: dictionary with open positions by instruments. 1539 """ 1540 if self.accountId is None or not self.accountId: 1541 uLogger.error("Variable `accountId` must be defined for using this method!") 1542 raise Exception("Account ID required") 1543 1544 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1545 1546 self.body = str({"accountId": self.accountId}) 1547 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1548 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1549 1550 if self.moreDebug: 1551 uLogger.debug("Records about current open positions successfully received") 1552 1553 return rawPositions 1554 1555 def RequestPendingOrders(self) -> list: 1556 """ 1557 Requesting current actual pending limit orders for current `accountId`. 1558 1559 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1560 1561 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1562 1563 :return: list of dictionaries with pending limit orders. 1564 """ 1565 if self.accountId is None or not self.accountId: 1566 uLogger.error("Variable `accountId` must be defined for using this method!") 1567 raise Exception("Account ID required") 1568 1569 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1570 1571 self.body = str({"accountId": self.accountId}) 1572 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1573 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1574 1575 if "orders" in rawResponse.keys(): 1576 rawOrders = rawResponse["orders"] 1577 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1578 1579 else: 1580 rawOrders = [] 1581 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1582 1583 return rawOrders 1584 1585 def RequestStopOrders(self) -> list: 1586 """ 1587 Requesting current actual stop orders for current `accountId`. 1588 1589 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1590 1591 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1592 1593 :return: list of dictionaries with stop orders. 1594 """ 1595 if self.accountId is None or not self.accountId: 1596 uLogger.error("Variable `accountId` must be defined for using this method!") 1597 raise Exception("Account ID required") 1598 1599 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1600 1601 self.body = str({"accountId": self.accountId}) 1602 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1603 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1604 1605 if "stopOrders" in rawResponse.keys(): 1606 rawStopOrders = rawResponse["stopOrders"] 1607 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1608 1609 else: 1610 rawStopOrders = [] 1611 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1612 1613 return rawStopOrders 1614 1615 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1616 """ 1617 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1618 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1619 and `overviewBondsCalendarFile` are defined then also save information to file. 1620 1621 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1622 many requests about the state of the portfolio, and then, based on the received data, a large number 1623 of calculation and statistics are collected. 1624 1625 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1626 :param details: how detailed should the information be? 1627 - `full` — shows full available information about portfolio status (by default), 1628 - `positions` — shows only open positions, 1629 - `orders` — shows only sections of open limits and stop orders. 1630 - `digest` — show a short digest of the portfolio status, 1631 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1632 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1633 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1634 :return: dictionary with client's raw portfolio and some statistics. 1635 """ 1636 if self.accountId is None or not self.accountId: 1637 uLogger.error("Variable `accountId` must be defined for using this method!") 1638 raise Exception("Account ID required") 1639 1640 view = { 1641 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1642 "headers": {}, # list of dictionaries, response headers without "positions" section 1643 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1644 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1645 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1646 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1647 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1648 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1649 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1650 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1651 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1652 }, 1653 "stat": { # --- some statistics calculated using "raw" sections: 1654 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1655 "availableRUB": 0., # available rubles (without other currencies) 1656 "blockedRUB": 0., # blocked sum in Russian Rouble 1657 "totalChangesRUB": 0., # changes for all open trades in RUB 1658 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1659 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1660 "sharesCostRUB": 0., # costs of all shares in RUB 1661 "bondsCostRUB": 0., # costs of all bonds in RUB 1662 "etfsCostRUB": 0., # costs of all etfs in RUB 1663 "futuresCostRUB": 0., # costs of all futures in RUB 1664 "Currencies": [], # list of dictionaries of all currencies statistics 1665 "Shares": [], # list of dictionaries of all shares statistics 1666 "Bonds": [], # list of dictionaries of all bonds statistics 1667 "Etfs": [], # list of dictionaries of all etfs statistics 1668 "Futures": [], # list of dictionaries of all futures statistics 1669 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1670 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1671 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1672 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1673 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1674 }, 1675 "analytics": { # --- some analytics of portfolio: 1676 "distrByAssets": {}, # portfolio distribution by assets 1677 "distrByCompanies": {}, # portfolio distribution by companies 1678 "distrBySectors": {}, # portfolio distribution by sectors 1679 "distrByCurrencies": {}, # portfolio distribution by currencies 1680 "distrByCountries": {}, # portfolio distribution by countries 1681 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1682 } 1683 } 1684 1685 details = details.lower() 1686 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1687 if details not in availableDetails: 1688 details = "full" 1689 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1690 1691 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1692 1693 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1694 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1695 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1696 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1697 1698 # save response headers without "positions" section: 1699 for key in portfolioResponse.keys(): 1700 if key != "positions": 1701 view["raw"]["headers"][key] = portfolioResponse[key] 1702 1703 else: 1704 continue 1705 1706 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1707 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1708 for item in portfolioResponse["positions"]: 1709 if item["instrumentType"] == "currency": 1710 self._figi = item["figi"] 1711 if not self._figi and item["ticker"]: 1712 self._ticker = item["ticker"] 1713 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1714 1715 curr = self.SearchByFIGI(requestPrice=False) 1716 1717 # current price of currency in RUB: 1718 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1719 "name": curr["name"], 1720 "currentPrice": NanoToFloat( 1721 item["currentPrice"]["units"], 1722 item["currentPrice"]["nano"] 1723 ), 1724 } 1725 1726 view["raw"]["Currencies"].append(item) 1727 1728 elif item["instrumentType"] == "share": 1729 view["raw"]["Shares"].append(item) 1730 1731 elif item["instrumentType"] == "bond": 1732 view["raw"]["Bonds"].append(item) 1733 1734 elif item["instrumentType"] == "etf": 1735 view["raw"]["Etfs"].append(item) 1736 1737 elif item["instrumentType"] == "futures": 1738 view["raw"]["Futures"].append(item) 1739 1740 else: 1741 continue 1742 1743 # how many volume of currencies (by ISO currency name) are blocked: 1744 for item in view["raw"]["positions"]["blocked"]: 1745 blocked = NanoToFloat(item["units"], item["nano"]) 1746 if blocked > 0: 1747 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1748 1749 # how many volume of instruments (by FIGI) are blocked: 1750 for item in view["raw"]["positions"]["securities"]: 1751 blocked = int(item["blocked"]) 1752 if blocked > 0: 1753 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1754 1755 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1756 1757 if "rub" in allBlocked.keys(): 1758 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1759 1760 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1761 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1762 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1763 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1764 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1765 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1766 view["stat"]["portfolioCostRUB"] = sum([ 1767 view["stat"]["allCurrenciesCostRUB"], 1768 view["stat"]["sharesCostRUB"], 1769 view["stat"]["bondsCostRUB"], 1770 view["stat"]["etfsCostRUB"], 1771 view["stat"]["futuresCostRUB"], 1772 ]) 1773 1774 # --- calculating some portfolio statistics: 1775 byComp = {} # distribution by companies 1776 bySect = {} # distribution by sectors 1777 byCurr = {} # distribution by currencies (include RUB) 1778 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1779 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1780 1781 for item in portfolioResponse["positions"]: 1782 self._figi = item["figi"] 1783 if not self._figi and item["ticker"]: 1784 self._ticker = item["ticker"] 1785 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1786 1787 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1788 1789 if instrument: 1790 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1791 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1792 1793 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1794 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1795 1796 else: 1797 blocked = 0 1798 1799 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1800 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1801 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1802 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1803 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1804 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1805 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1806 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1807 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1808 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1809 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1810 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1811 1812 statData = { 1813 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1814 "ticker": instrument["ticker"], # ticker by FIGI 1815 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1816 "volume": volume, # available volume of instrument 1817 "lots": lots, # volume in lots of instrument 1818 "direction": direction, # direction of an instrument's position: short or long 1819 "blocked": blocked, # blocked volume of currency or instrument 1820 "currentPrice": curPrice, # current instrument's price in basic asset 1821 "average": average, # current average position price 1822 "cost": cost, # current cost of all volume of instrument in basic asset 1823 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1824 "costRUB": costRUB, # cost of instrument in ruble 1825 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1826 "profit": profit, # expected profit at current moment 1827 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1828 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1829 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1830 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1831 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1832 "step": instrument["step"], # minimum price increment 1833 } 1834 1835 # adding distribution by unique countries: 1836 if statData["country"] not in byCountry.keys(): 1837 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1838 1839 else: 1840 byCountry[statData["country"]]["cost"] += costRUB 1841 byCountry[statData["country"]]["percent"] += percentCostRUB 1842 1843 if item["instrumentType"] != "currency": 1844 # adding distribution by unique companies: 1845 if statData["name"]: 1846 if statData["name"] not in byComp.keys(): 1847 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1848 1849 else: 1850 byComp[statData["name"]]["cost"] += costRUB 1851 byComp[statData["name"]]["percent"] += percentCostRUB 1852 1853 # adding distribution by unique sectors: 1854 if statData["sector"] not in bySect.keys(): 1855 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1856 1857 else: 1858 bySect[statData["sector"]]["cost"] += costRUB 1859 bySect[statData["sector"]]["percent"] += percentCostRUB 1860 1861 # adding distribution by unique currencies: 1862 if currency not in byCurr.keys(): 1863 byCurr[currency] = { 1864 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1865 "cost": costRUB, 1866 "percent": percentCostRUB 1867 } 1868 1869 else: 1870 byCurr[currency]["cost"] += costRUB 1871 byCurr[currency]["percent"] += percentCostRUB 1872 1873 # saving statistics for every instrument: 1874 if item["instrumentType"] == "currency": 1875 view["stat"]["Currencies"].append(statData) 1876 1877 # update dict with free funds for trading (total - blocked) by currencies 1878 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1879 view["stat"]["funds"][currency] = { 1880 "total": volume, 1881 "totalCostRUB": costRUB, # total volume cost in rubles 1882 "free": volume - blocked, 1883 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1884 } 1885 1886 elif item["instrumentType"] == "share": 1887 view["stat"]["Shares"].append(statData) 1888 1889 elif item["instrumentType"] == "bond": 1890 view["stat"]["Bonds"].append(statData) 1891 1892 elif item["instrumentType"] == "etf": 1893 view["stat"]["Etfs"].append(statData) 1894 1895 elif item["instrumentType"] == "Futures": 1896 view["stat"]["Futures"].append(statData) 1897 1898 else: 1899 continue 1900 1901 # total changes in Russian Ruble: 1902 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1903 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1904 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1905 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1906 view["stat"]["funds"]["rub"] = { 1907 "total": view["stat"]["availableRUB"], 1908 "totalCostRUB": view["stat"]["availableRUB"], 1909 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1910 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1911 } 1912 1913 # --- pending limit orders sector data: 1914 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1915 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1916 1917 for item in view["raw"]["orders"]: 1918 self._figi = item["figi"] 1919 1920 if item["figi"] not in uniquePendingOrdersFIGIs: 1921 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1922 1923 uniquePendingOrdersFIGIs.append(item["figi"]) 1924 uniquePendingOrders[item["figi"]] = instrument 1925 1926 else: 1927 instrument = uniquePendingOrders[item["figi"]] 1928 1929 if instrument: 1930 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1931 orderType = TKS_ORDER_TYPES[item["orderType"]] 1932 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1933 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1934 1935 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1936 if item["direction"] == "ORDER_DIRECTION_BUY": 1937 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1938 1939 else: 1940 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1941 1942 # requested price for order execution: 1943 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1944 1945 # necessary changes in percent to reach target from current price: 1946 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1947 1948 view["stat"]["orders"].append({ 1949 "orderID": item["orderId"], # orderId number parameter of current order 1950 "figi": item["figi"], # FIGI identification 1951 "ticker": instrument["ticker"], # ticker name by FIGI 1952 "lotsRequested": item["lotsRequested"], # requested lots value 1953 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1954 "currentPrice": lastPrice, # current instrument's price for defined action 1955 "targetPrice": target, # requested price for order execution in base currency 1956 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1957 "percentChanges": changes, # changes in percent to target from current price 1958 "currency": item["currency"], # instrument's currency name 1959 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1960 "type": orderType, # type of order from TKS_ORDER_TYPES 1961 "status": orderState, # order status from TKS_ORDER_STATES 1962 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1963 }) 1964 1965 # --- stop orders sector data: 1966 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1967 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1968 1969 for item in view["raw"]["stopOrders"]: 1970 self._figi = item["figi"] 1971 1972 if item["figi"] not in uniqueStopOrdersFIGIs: 1973 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1974 1975 uniqueStopOrdersFIGIs.append(item["figi"]) 1976 uniqueStopOrders[item["figi"]] = instrument 1977 1978 else: 1979 instrument = uniqueStopOrders[item["figi"]] 1980 1981 if instrument: 1982 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1983 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1984 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1985 1986 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1987 if "expirationTime" in item.keys(): 1988 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1989 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1990 1991 else: 1992 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1993 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1994 1995 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1996 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1997 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1998 1999 else: 2000 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2001 2002 # requested price when stop-order executed: 2003 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2004 2005 # price for limit-order, set up when stop-order executed: 2006 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2007 2008 # necessary changes in percent to reach target from current price: 2009 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2010 2011 view["stat"]["stopOrders"].append({ 2012 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2013 "figi": item["figi"], # FIGI identification 2014 "ticker": instrument["ticker"], # ticker name by FIGI 2015 "lotsRequested": item["lotsRequested"], # requested lots value 2016 "currentPrice": lastPrice, # current instrument's price for defined action 2017 "targetPrice": target, # requested price for stop-order execution in base currency 2018 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2019 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2020 "percentChanges": changes, # changes in percent to target from current price 2021 "currency": item["currency"], # instrument's currency name 2022 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2023 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2024 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2025 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2026 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2027 }) 2028 2029 # --- calculating data for analytics section: 2030 # portfolio distribution by assets: 2031 view["analytics"]["distrByAssets"] = { 2032 "Ruble": { 2033 "uniques": 1, 2034 "cost": view["stat"]["availableRUB"], 2035 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2036 }, 2037 "Currencies": { 2038 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2039 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2040 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2041 }, 2042 "Shares": { 2043 "uniques": len(view["stat"]["Shares"]), 2044 "cost": view["stat"]["sharesCostRUB"], 2045 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2046 }, 2047 "Bonds": { 2048 "uniques": len(view["stat"]["Bonds"]), 2049 "cost": view["stat"]["bondsCostRUB"], 2050 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2051 }, 2052 "Etfs": { 2053 "uniques": len(view["stat"]["Etfs"]), 2054 "cost": view["stat"]["etfsCostRUB"], 2055 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2056 }, 2057 "Futures": { 2058 "uniques": len(view["stat"]["Futures"]), 2059 "cost": view["stat"]["futuresCostRUB"], 2060 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2061 }, 2062 } 2063 2064 # portfolio distribution by companies: 2065 view["analytics"]["distrByCompanies"]["All money cash"] = { 2066 "ticker": "", 2067 "cost": view["stat"]["allCurrenciesCostRUB"], 2068 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2069 } 2070 view["analytics"]["distrByCompanies"].update(byComp) 2071 2072 # portfolio distribution by sectors: 2073 view["analytics"]["distrBySectors"]["All money cash"] = { 2074 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2075 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2076 } 2077 view["analytics"]["distrBySectors"].update(bySect) 2078 2079 # portfolio distribution by currencies: 2080 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2081 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2082 2083 if self.moreDebug: 2084 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2085 2086 view["analytics"]["distrByCurrencies"].update(byCurr) 2087 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2088 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2089 2090 # portfolio distribution by countries: 2091 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2092 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2093 2094 if self.moreDebug: 2095 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2096 2097 view["analytics"]["distrByCountries"].update(byCountry) 2098 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2099 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2100 2101 # --- Prepare text statistics overview in human-readable: 2102 if show or onlyFiles: 2103 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2104 2105 # Whatever the value `details`, header not changes: 2106 info = [ 2107 "# Client's portfolio\n\n", 2108 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2109 "* **Account ID:** [{}]\n".format(self.accountId), 2110 ] 2111 2112 if details in ["full", "positions", "digest"]: 2113 info.extend([ 2114 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2115 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2116 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2117 view["stat"]["totalChangesRUB"], 2118 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2119 view["stat"]["totalChangesPercentRUB"], 2120 ), 2121 ]) 2122 2123 if details in ["full", "positions"]: 2124 info.extend([ 2125 "## Open positions\n\n", 2126 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2127 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2128 "| **Ruble:** | {:>31} | | | | | |\n".format( 2129 "{:.2f} ({:.2f}) rub".format( 2130 view["stat"]["availableRUB"], 2131 view["stat"]["blockedRUB"], 2132 ) 2133 ) 2134 ]) 2135 2136 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2137 return [ 2138 "| | | | | | | |\n", 2139 "| {:<27} | | | | | {:>19} | |\n".format( 2140 noTradeStr if noTradeStr else typeStr, 2141 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2142 ), 2143 ] 2144 2145 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2146 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2147 "{} [{}]".format(data["ticker"], data["figi"]), 2148 "{:.2f} ({:.2f}) {}".format( 2149 data["volume"], 2150 data["blocked"], 2151 data["currency"], 2152 ) if isCurr else "{:.0f} ({:.0f})".format( 2153 data["volume"], 2154 data["blocked"], 2155 ), 2156 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2157 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2158 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2159 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2160 "{}{:.2f} {} ({}{:.2f}%)".format( 2161 "+" if data["profit"] > 0 else "", 2162 data["profit"], data["baseCurrencyName"], 2163 "+" if data["percentProfit"] > 0 else "", 2164 data["percentProfit"], 2165 ), 2166 ) 2167 2168 # --- Show currencies section: 2169 if view["stat"]["Currencies"]: 2170 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2171 for item in view["stat"]["Currencies"]: 2172 info.append(_InfoStr(item, isCurr=True)) 2173 2174 else: 2175 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2176 2177 # --- Show shares section: 2178 if view["stat"]["Shares"]: 2179 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2180 2181 for item in view["stat"]["Shares"]: 2182 info.append(_InfoStr(item)) 2183 2184 else: 2185 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2186 2187 # --- Show bonds section: 2188 if view["stat"]["Bonds"]: 2189 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2190 2191 for item in view["stat"]["Bonds"]: 2192 info.append(_InfoStr(item)) 2193 2194 else: 2195 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2196 2197 # --- Show etfs section: 2198 if view["stat"]["Etfs"]: 2199 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2200 2201 for item in view["stat"]["Etfs"]: 2202 info.append(_InfoStr(item)) 2203 2204 else: 2205 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2206 2207 # --- Show futures section: 2208 if view["stat"]["Futures"]: 2209 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2210 2211 for item in view["stat"]["Futures"]: 2212 info.append(_InfoStr(item)) 2213 2214 else: 2215 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2216 2217 if details in ["full", "orders"]: 2218 # --- Show pending limit orders section: 2219 if view["stat"]["orders"]: 2220 info.extend([ 2221 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2222 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2223 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2224 ]) 2225 2226 for item in view["stat"]["orders"]: 2227 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2228 "{} [{}]".format(item["ticker"], item["figi"]), 2229 item["orderID"], 2230 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2231 "{} {} ({}{:.2f}%)".format( 2232 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2233 item["baseCurrencyName"], 2234 "+" if item["percentChanges"] > 0 else "", 2235 float(item["percentChanges"]), 2236 ), 2237 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2238 item["action"], 2239 item["type"], 2240 item["date"], 2241 )) 2242 2243 else: 2244 info.append("\n## Total pending limit-orders: [0]\n") 2245 2246 # --- Show stop orders section: 2247 if view["stat"]["stopOrders"]: 2248 info.extend([ 2249 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2250 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2251 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2252 ]) 2253 2254 for item in view["stat"]["stopOrders"]: 2255 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2256 "{} [{}]".format(item["ticker"], item["figi"]), 2257 item["orderID"], 2258 item["lotsRequested"], 2259 "{} {} ({}{:.2f}%)".format( 2260 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2261 item["baseCurrencyName"], 2262 "+" if item["percentChanges"] > 0 else "", 2263 float(item["percentChanges"]), 2264 ), 2265 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2266 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2267 item["action"], 2268 item["type"], 2269 item["expType"], 2270 item["createDate"], 2271 item["expDate"], 2272 )) 2273 2274 else: 2275 info.append("\n## Total stop-orders: [0]\n") 2276 2277 if details in ["full", "analytics"]: 2278 # -- Show analytics section: 2279 if view["stat"]["portfolioCostRUB"] > 0: 2280 info.extend([ 2281 "\n# Analytics\n\n" 2282 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2283 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2284 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2285 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2286 view["stat"]["totalChangesRUB"], 2287 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2288 view["stat"]["totalChangesPercentRUB"], 2289 ), 2290 "\n## Portfolio distribution by assets\n" 2291 "\n| Type | Uniques | Percent | Current cost |\n", 2292 "|------------------------------------|---------|---------|--------------------|\n", 2293 ]) 2294 2295 for key in view["analytics"]["distrByAssets"].keys(): 2296 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2297 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2298 key, 2299 view["analytics"]["distrByAssets"][key]["uniques"], 2300 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2301 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2302 )) 2303 2304 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2305 2306 info.extend([ 2307 "\n## Portfolio distribution by companies\n" 2308 "\n| Company | Percent | Current cost |\n", 2309 aSepLine, 2310 ]) 2311 2312 for company in view["analytics"]["distrByCompanies"].keys(): 2313 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2314 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2315 "{}{}".format( 2316 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2317 company, 2318 ), 2319 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2320 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2321 )) 2322 2323 info.extend([ 2324 "\n## Portfolio distribution by sectors\n" 2325 "\n| Sector | Percent | Current cost |\n", 2326 aSepLine, 2327 ]) 2328 2329 for sector in view["analytics"]["distrBySectors"].keys(): 2330 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2331 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2332 sector, 2333 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2334 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2335 )) 2336 2337 info.extend([ 2338 "\n## Portfolio distribution by currencies\n" 2339 "\n| Instruments currencies | Percent | Current cost |\n", 2340 aSepLine, 2341 ]) 2342 2343 for curr in view["analytics"]["distrByCurrencies"].keys(): 2344 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2345 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2346 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2347 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2348 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2349 )) 2350 2351 info.extend([ 2352 "\n## Portfolio distribution by countries\n" 2353 "\n| Assets by country | Percent | Current cost |\n", 2354 aSepLine, 2355 ]) 2356 2357 for country in view["analytics"]["distrByCountries"].keys(): 2358 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2359 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2360 country, 2361 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2362 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2363 )) 2364 2365 if details in ["full", "calendar"]: 2366 # -- Show bonds payment calendar section: 2367 if view["stat"]["Bonds"]: 2368 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2369 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2370 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2371 2372 else: 2373 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2374 2375 infoText = "".join(info) 2376 2377 if show and not onlyFiles: 2378 uLogger.info(infoText) 2379 2380 if details == "full" and self.overviewFile: 2381 filename = self.overviewFile 2382 2383 elif details == "digest" and self.overviewDigestFile: 2384 filename = self.overviewDigestFile 2385 2386 elif details == "positions" and self.overviewPositionsFile: 2387 filename = self.overviewPositionsFile 2388 2389 elif details == "orders" and self.overviewOrdersFile: 2390 filename = self.overviewOrdersFile 2391 2392 elif details == "analytics" and self.overviewAnalyticsFile: 2393 filename = self.overviewAnalyticsFile 2394 2395 elif details == "calendar" and self.overviewBondsCalendarFile: 2396 filename = self.overviewBondsCalendarFile 2397 2398 else: 2399 filename = "" 2400 2401 if filename and (show or onlyFiles): 2402 with open(filename, "w", encoding="UTF-8") as fH: 2403 fH.write(infoText) 2404 2405 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2406 2407 if self.useHTMLReports: 2408 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2409 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2410 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2411 2412 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2413 2414 return view 2415 2416 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2417 """ 2418 Returns history operations between two given dates for current `accountId`. 2419 If `reportFile` string is not empty then also save human-readable report. 2420 Shows some statistical data of closed positions. 2421 2422 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2423 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2424 :param show: if `True` then also prints all records to the console. 2425 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2426 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2427 :return: original list of dictionaries with history of deals records from API ("operations" key): 2428 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2429 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2430 """ 2431 if self.accountId is None or not self.accountId: 2432 uLogger.error("Variable `accountId` must be defined for using this method!") 2433 raise Exception("Account ID required") 2434 2435 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2436 2437 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2438 2439 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2440 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2441 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2442 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2443 customStat = {} # custom statistics in additional to responseJSON 2444 2445 # --- output report in human-readable format: 2446 if show or onlyFiles or self.reportFile: 2447 splitLine1 = "| | | | | |\n" # Summary section 2448 splitLine2 = "| | | | | | | | |\n" # Operations section 2449 nextDay = "" 2450 2451 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2452 2453 if len(ops) > 0: 2454 customStat = { 2455 "opsCount": 0, # total operations count 2456 "buyCount": 0, # buy operations 2457 "sellCount": 0, # sell operations 2458 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2459 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2460 "payIn": {"rub": 0.}, # Deposit brokerage account 2461 "payOut": {"rub": 0.}, # Withdrawals 2462 "divs": {"rub": 0.}, # Dividends income 2463 "coupons": {"rub": 0.}, # Coupon's income 2464 "brokerCom": {"rub": 0.}, # Service commissions 2465 "serviceCom": {"rub": 0.}, # Service commissions 2466 "marginCom": {"rub": 0.}, # Margin commissions 2467 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2468 } 2469 2470 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2471 for item in ops: 2472 if item["state"] == "OPERATION_STATE_EXECUTED": 2473 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2474 2475 # count buy operations: 2476 if "_BUY" in item["operationType"]: 2477 customStat["buyCount"] += 1 2478 2479 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2480 customStat["buyTotal"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["buyTotal"][item["payment"]["currency"]] = payment 2484 2485 # count sell operations: 2486 elif "_SELL" in item["operationType"]: 2487 customStat["sellCount"] += 1 2488 2489 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2490 customStat["sellTotal"][item["payment"]["currency"]] += payment 2491 2492 else: 2493 customStat["sellTotal"][item["payment"]["currency"]] = payment 2494 2495 # count incoming operations: 2496 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2497 if item["payment"]["currency"] in customStat["payIn"].keys(): 2498 customStat["payIn"][item["payment"]["currency"]] += payment 2499 2500 else: 2501 customStat["payIn"][item["payment"]["currency"]] = payment 2502 2503 # count withdrawals operations: 2504 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2505 if item["payment"]["currency"] in customStat["payOut"].keys(): 2506 customStat["payOut"][item["payment"]["currency"]] += payment 2507 2508 else: 2509 customStat["payOut"][item["payment"]["currency"]] = payment 2510 2511 # count dividends income: 2512 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2513 if item["payment"]["currency"] in customStat["divs"].keys(): 2514 customStat["divs"][item["payment"]["currency"]] += payment 2515 2516 else: 2517 customStat["divs"][item["payment"]["currency"]] = payment 2518 2519 # count coupon's income: 2520 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2521 if item["payment"]["currency"] in customStat["coupons"].keys(): 2522 customStat["coupons"][item["payment"]["currency"]] += payment 2523 2524 else: 2525 customStat["coupons"][item["payment"]["currency"]] = payment 2526 2527 # count broker commissions: 2528 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2529 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2530 customStat["brokerCom"][item["payment"]["currency"]] += payment 2531 2532 else: 2533 customStat["brokerCom"][item["payment"]["currency"]] = payment 2534 2535 # count service commissions: 2536 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2537 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2538 customStat["serviceCom"][item["payment"]["currency"]] += payment 2539 2540 else: 2541 customStat["serviceCom"][item["payment"]["currency"]] = payment 2542 2543 # count margin commissions: 2544 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2545 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2546 customStat["marginCom"][item["payment"]["currency"]] += payment 2547 2548 else: 2549 customStat["marginCom"][item["payment"]["currency"]] = payment 2550 2551 # count withholding taxes: 2552 elif "_TAX" in item["operationType"]: 2553 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2554 customStat["allTaxes"][item["payment"]["currency"]] += payment 2555 2556 else: 2557 customStat["allTaxes"][item["payment"]["currency"]] = payment 2558 2559 else: 2560 continue 2561 2562 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2563 2564 # --- view "Actions" lines: 2565 info.extend([ 2566 "| Report sections | | | | |\n", 2567 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2568 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2569 "| | Buy: {:<22} | {:<28} | | |\n".format( 2570 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2571 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2572 ), 2573 "| | Sell: {:<21} | {:<28} | | |\n".format( 2574 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2575 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2576 ), 2577 ]) 2578 2579 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2580 for key in opsKeys: 2581 if key == "rub": 2582 continue 2583 2584 info.extend([ 2585 "| | | {:<28} | | |\n".format( 2586 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2587 ), 2588 "| | | {:<28} | | |\n".format( 2589 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2590 ), 2591 ]) 2592 2593 info.append(splitLine1) 2594 2595 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2596 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2597 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2598 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2599 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2600 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2601 ) 2602 2603 # --- view "Payments" lines: 2604 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2605 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2606 2607 for key in paymentsKeys: 2608 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2609 2610 info.append(splitLine1) 2611 2612 # --- view "Commissions and taxes" lines: 2613 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2614 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2615 2616 for key in comKeys: 2617 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2618 2619 info.extend([ 2620 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2621 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2622 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2623 ]) 2624 2625 else: 2626 info.append("Broker returned no operations during this period\n") 2627 2628 # --- view "Operations" section: 2629 for item in ops: 2630 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2631 continue 2632 2633 else: 2634 self._figi = item["figi"] 2635 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2636 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2637 2638 # group of deals during one day: 2639 if nextDay and item["date"].split("T")[0] != nextDay: 2640 info.append(splitLine2) 2641 nextDay = "" 2642 2643 else: 2644 nextDay = item["date"].split("T")[0] # saving current day for splitting 2645 2646 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2647 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2648 self._figi if self._figi else "—", 2649 instrument["ticker"] if instrument else "—", 2650 instrument["type"] if instrument else "—", 2651 item["quantity"] if int(item["quantity"]) > 0 else "—", 2652 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2653 TKS_OPERATION_STATES[item["state"]], 2654 TKS_OPERATION_TYPES[item["operationType"]], 2655 )) 2656 2657 infoText = "".join(info) 2658 2659 if show and not onlyFiles: 2660 if self.moreDebug: 2661 uLogger.debug("Records about history of a client's operations successfully received") 2662 2663 uLogger.info(infoText) 2664 2665 if self.reportFile and (show or onlyFiles): 2666 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2667 fH.write(infoText) 2668 2669 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2670 2671 if self.useHTMLReports: 2672 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2673 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2674 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2675 2676 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2677 2678 return ops, customStat 2679 2680 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2681 """ 2682 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2683 2684 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2685 Warning! Broker server used ISO UTC time by default. 2686 2687 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2688 Also, `historyFile` used to update history with `onlyMissing` parameter. 2689 2690 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2691 2692 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2693 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2694 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2695 `"hour"`, `"day"`. Default: `"hour"`. 2696 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2697 False by default. Warning! History appends only from last candle to current time 2698 with always update last candle! 2699 :param csvSep: separator if csv-file is used, `,` by default. 2700 :param show: if `True` then also prints Pandas DataFrame to the console. 2701 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2702 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2703 `["date", "time", "open", "high", "low", "close", "volume"]`. 2704 """ 2705 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2706 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2707 history = None # empty pandas object for history 2708 2709 if interval not in TKS_CANDLE_INTERVALS.keys(): 2710 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2711 raise Exception("Incorrect value") 2712 2713 if not (self._ticker or self._figi): 2714 uLogger.error("Ticker or FIGI must be defined!") 2715 raise Exception("Ticker or FIGI required") 2716 2717 if self._ticker and not self._figi: 2718 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2719 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2720 2721 if self._figi and not self._ticker: 2722 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2723 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2724 2725 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2726 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2727 if interval.lower() != "day": 2728 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2729 2730 delta = dtEnd - dtStart # current UTC time minus last time in file 2731 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2732 2733 # calculate history length in candles: 2734 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2735 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2736 length += 1 # to avoid fraction time 2737 2738 # calculate data blocks count: 2739 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2740 2741 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2742 if self.moreDebug: 2743 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2744 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2745 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2746 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2747 2748 tempOld = None # pandas object for old history, if --only-missing key present 2749 lastTime = None # datetime object of last old candle in file 2750 2751 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2752 if self.moreDebug: 2753 uLogger.debug("--only-missing key present, add only last missing candles...") 2754 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2755 2756 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2757 2758 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2759 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2760 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2761 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2762 2763 # get last datetime object from last string in file or minus 1 delta if file is empty: 2764 if len(tempOld) > 0: 2765 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2766 2767 else: 2768 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2769 2770 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2771 2772 responseJSONs = [] # raw history blocks of data 2773 2774 blockEnd = dtEnd 2775 for item in range(blocks): 2776 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2777 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2778 2779 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2780 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2781 )) 2782 2783 if blockStart == blockEnd: 2784 uLogger.debug("Skipped this zero-length block...") 2785 2786 else: 2787 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2788 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2789 self.body = str({ 2790 "figi": self._figi, 2791 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2792 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2793 "interval": TKS_CANDLE_INTERVALS[interval][0] 2794 }) 2795 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2796 2797 if "code" in responseJSON.keys(): 2798 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2799 2800 else: 2801 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2802 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2803 2804 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2805 2806 blockEnd = blockStart 2807 2808 printCount = len(responseJSONs) # candles to show in console 2809 if responseJSONs: 2810 tempHistory = pd.DataFrame( 2811 data={ 2812 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2813 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2814 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2815 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2816 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2817 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2818 "volume": [int(item["volume"]) for item in responseJSONs], 2819 }, 2820 index=range(len(responseJSONs)), 2821 columns=["date", "time", "open", "high", "low", "close", "volume"], 2822 ) 2823 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2824 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2825 2826 # append only newest candles to old history if --only-missing key present: 2827 if onlyMissing and tempOld is not None and lastTime is not None: 2828 index = 0 # find start index in tempHistory data: 2829 2830 for i, item in tempHistory.iterrows(): 2831 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2832 2833 if curTime == lastTime: 2834 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2835 index = i 2836 printCount = index + 1 2837 break 2838 2839 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2840 2841 else: 2842 history = tempHistory # if no `--only-missing` key then load full data from server 2843 2844 if self.moreDebug: 2845 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2846 2847 if history is not None and not history.empty: 2848 if show and not onlyFiles: 2849 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2850 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2851 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2852 )) 2853 2854 else: 2855 uLogger.warning("Received an empty candles history!") 2856 2857 if self.historyFile is not None: 2858 if history is not None and not history.empty: 2859 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2860 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2861 2862 else: 2863 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2864 2865 else: 2866 if self.moreDebug: 2867 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2868 2869 return history 2870 2871 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2872 """ 2873 Load candles history from csv-file and return Pandas DataFrame object. 2874 2875 See also: `History()` and `ShowHistoryChart()` methods. 2876 2877 :param filePath: path to csv-file to open. 2878 """ 2879 loadedHistory = None # init candles data object 2880 2881 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2882 2883 if os.path.exists(filePath): 2884 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2885 2886 tfStr = self.priceModel.FormattedDelta( 2887 self.priceModel.timeframe, 2888 "{days} days {hours}h {minutes}m {seconds}s", 2889 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2890 self.priceModel.timeframe, 2891 "{hours}h {minutes}m {seconds}s", 2892 ) 2893 2894 if loadedHistory is not None and not loadedHistory.empty: 2895 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2896 len(loadedHistory), 2897 tfStr, 2898 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2899 ) 2900 2901 else: 2902 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2903 2904 else: 2905 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2906 2907 return loadedHistory 2908 2909 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2910 """ 2911 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2912 2913 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2914 Default: `index.html` (both for interact and non-interact candlesticks chart). 2915 2916 See also: `History()` and `LoadHistory()` methods. 2917 2918 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2919 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2920 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2921 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2922 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2923 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2924 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2925 """ 2926 if isinstance(candles, str): 2927 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2928 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2929 2930 elif isinstance(candles, pd.DataFrame): 2931 self.priceModel.prices = candles # set candles chain from variable 2932 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2933 2934 if "datetime" not in candles.columns: 2935 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2936 2937 else: 2938 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2939 raise Exception("Incorrect value") 2940 2941 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2942 2943 if interact: 2944 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2945 2946 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2947 2948 else: 2949 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2950 2951 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2952 2953 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2954 2955 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2956 """ 2957 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2958 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2959 2960 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2961 2962 :param operation: string "Buy" or "Sell". 2963 :param lots: volume, integer count of lots >= 1. 2964 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2965 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2966 :param expDate: string "Undefined" by default or local date in future, 2967 it is a string with format `%Y-%m-%d %H:%M:%S`. 2968 :return: JSON with response from broker server. 2969 """ 2970 if self.accountId is None or not self.accountId: 2971 uLogger.error("Variable `accountId` must be defined for using this method!") 2972 raise Exception("Account ID required") 2973 2974 if operation is None or not operation or operation not in ("Buy", "Sell"): 2975 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2976 raise Exception("Incorrect value") 2977 2978 if lots is None or lots < 1: 2979 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2980 lots = 1 2981 2982 if tp is None or tp < 0: 2983 tp = 0 2984 2985 if sl is None or sl < 0: 2986 sl = 0 2987 2988 if expDate is None or not expDate: 2989 expDate = "Undefined" 2990 2991 if not (self._ticker or self._figi): 2992 uLogger.error("Ticker or FIGI must be defined!") 2993 raise Exception("Ticker or FIGI required") 2994 2995 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2996 self._ticker = instrument["ticker"] 2997 self._figi = instrument["figi"] 2998 2999 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3000 3001 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3002 self.body = str({ 3003 "figi": self._figi, 3004 "quantity": str(lots), 3005 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3006 "accountId": str(self.accountId), 3007 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3008 }) 3009 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3010 3011 if "orderId" in response.keys(): 3012 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3013 operation, response["orderId"], 3014 self._ticker, self._figi, lots, 3015 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3016 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3017 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3018 )) 3019 3020 if tp > 0: 3021 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3022 3023 if sl > 0: 3024 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3025 3026 else: 3027 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3028 3029 return response 3030 3031 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3032 """ 3033 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3034 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3035 3036 See also: `Order()` and `Trade()` docstrings. 3037 3038 :param lots: volume, integer count of lots >= 1. 3039 :param tp: float > 0, take profit price of stop-order. 3040 :param sl: float > 0, stop loss price of stop-order. 3041 :param expDate: it's a local date in future. 3042 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3043 :return: JSON with response from broker server. 3044 """ 3045 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3046 3047 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3048 """ 3049 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3050 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3051 3052 See also: `Order()` and `Trade()` docstrings. 3053 3054 :param lots: volume, integer count of lots >= 1. 3055 :param tp: float > 0, take profit price of stop-order. 3056 :param sl: float > 0, stop loss price of stop-order. 3057 :param expDate: it's a local date in the future. 3058 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3059 :return: JSON with response from broker server. 3060 """ 3061 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3062 3063 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3064 """ 3065 Close position of given instruments. 3066 3067 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3068 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3069 This avoids unnecessary downloading data from the server. 3070 """ 3071 if instruments is None or not instruments: 3072 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3073 raise Exception("Ticker or FIGI required") 3074 3075 if isinstance(instruments, str): 3076 instruments = [instruments] 3077 3078 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3079 if uniqueInstruments: 3080 if portfolio is None or not portfolio: 3081 portfolio = self.Overview(show=False) 3082 3083 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3084 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3085 3086 for self._figi in uniqueInstruments: 3087 if self._figi not in allOpened: 3088 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3089 continue 3090 3091 # search open trade info about instrument by ticker: 3092 instrument = {} 3093 for iType in TKS_INSTRUMENTS: 3094 if instrument: 3095 break 3096 3097 for item in portfolio["stat"][iType]: 3098 if item["figi"] == self._figi: 3099 instrument = item 3100 break 3101 3102 if instrument: 3103 self._ticker = instrument["ticker"] 3104 self._figi = instrument["figi"] 3105 3106 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3107 self._ticker, 3108 self._figi, 3109 int(instrument["volume"]), 3110 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3111 )) 3112 3113 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3114 3115 if tradeLots > 0: 3116 if instrument["blocked"] > 0: 3117 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3118 instrument["blocked"], 3119 self._ticker, 3120 tradeLots, 3121 )) 3122 3123 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3124 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3125 3126 else: 3127 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3128 3129 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3130 """ 3131 Close all positions of given instruments with defined type. 3132 3133 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3134 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3135 This avoids unnecessary downloading data from the server. 3136 """ 3137 if iType not in TKS_INSTRUMENTS: 3138 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3139 3140 else: 3141 if portfolio is None or not portfolio: 3142 portfolio = self.Overview(show=False) 3143 3144 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3145 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3146 3147 if tickers and portfolio: 3148 self.CloseTrades(tickers, portfolio) 3149 3150 else: 3151 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3152 3153 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3154 """ 3155 Universal method to create market or limit orders with all available parameters for current `accountId`. 3156 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3157 3158 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3159 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3160 3161 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3162 then broker immediately open market order as you can do simple --buy or --sell operations! 3163 3164 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3165 When current price will go up or down to target price value then broker opens a limit order. 3166 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3167 3168 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3169 3170 :param operation: string "Buy" or "Sell". 3171 :param orderType: string "Limit" or "Stop". 3172 :param lots: volume, integer count of lots >= 1. 3173 :param targetPrice: target price > 0. This is open trade price for limit order. 3174 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3175 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3176 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3177 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3178 Stop loss order always executed by market price. 3179 :param expDate: string "Undefined" by default or local date in future. 3180 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3181 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3182 A limit order has no expiration date, it lasts until the end of the trading day. 3183 :return: JSON with response from broker server. 3184 """ 3185 if self.accountId is None or not self.accountId: 3186 uLogger.error("Variable `accountId` must be defined for using this method!") 3187 raise Exception("Account ID required") 3188 3189 if operation is None or not operation or operation not in ("Buy", "Sell"): 3190 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3191 raise Exception("Incorrect value") 3192 3193 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3194 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3195 raise Exception("Incorrect value") 3196 3197 if lots is None or lots < 1: 3198 uLogger.error("You must define trade volume > 0: integer count of lots!") 3199 raise Exception("Incorrect value") 3200 3201 if targetPrice is None or targetPrice <= 0: 3202 uLogger.error("Target price for limit-order must be greater than 0!") 3203 raise Exception("Incorrect value") 3204 3205 if limitPrice is None or limitPrice <= 0: 3206 limitPrice = targetPrice 3207 3208 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3209 stopType = "Limit" 3210 3211 if expDate is None or not expDate: 3212 expDate = "Undefined" 3213 3214 if not (self._ticker or self._figi): 3215 uLogger.error("Tocker or FIGI must be defined!") 3216 raise Exception("Ticker or FIGI required") 3217 3218 response = {} 3219 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3220 self._ticker = instrument["ticker"] 3221 self._figi = instrument["figi"] 3222 3223 if orderType == "Limit": 3224 uLogger.debug( 3225 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3226 self._ticker, self._figi, 3227 operation, lots, targetPrice, instrument["currency"], 3228 )) 3229 3230 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3231 self.body = str({ 3232 "figi": self._figi, 3233 "quantity": str(lots), 3234 "price": FloatToNano(targetPrice), 3235 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3236 "accountId": str(self.accountId), 3237 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3238 }) 3239 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3240 3241 if "orderId" in response.keys(): 3242 uLogger.info( 3243 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3244 response["orderId"], self._ticker, self._figi, operation, lots, 3245 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3246 )) 3247 3248 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3249 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3250 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3251 targetPrice, instrument["currency"], 3252 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3253 )) 3254 3255 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3256 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3257 targetPrice, instrument["currency"], 3258 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3259 )) 3260 3261 else: 3262 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3263 3264 if orderType == "Stop": 3265 uLogger.debug( 3266 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3267 self._ticker, self._figi, 3268 operation, lots, 3269 targetPrice, instrument["currency"], 3270 limitPrice, instrument["currency"], 3271 stopType, expDate, 3272 )) 3273 3274 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3275 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3276 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3277 3278 body = { 3279 "figi": self._figi, 3280 "quantity": str(lots), 3281 "price": FloatToNano(limitPrice), 3282 "stopPrice": FloatToNano(targetPrice), 3283 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3284 "accountId": str(self.accountId), 3285 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3286 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3287 } 3288 3289 if expDateUTC: 3290 body["expireDate"] = expDateUTC 3291 3292 self.body = str(body) 3293 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3294 3295 if "stopOrderId" in response.keys(): 3296 uLogger.info( 3297 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3298 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3299 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3300 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3301 TKS_STOP_ORDER_TYPES[stopOrderType], 3302 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3303 )) 3304 3305 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3306 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3307 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3308 targetPrice, instrument["currency"], 3309 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3310 )) 3311 3312 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3313 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3314 targetPrice, instrument["currency"], 3315 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3316 )) 3317 3318 else: 3319 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3320 3321 return response 3322 3323 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3324 """ 3325 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3326 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3327 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3328 See also: `Order()` docstring. 3329 3330 :param lots: volume, integer count of lots >= 1. 3331 :param targetPrice: target price > 0. This is open trade price for limit order. 3332 :return: JSON with response from broker server. 3333 """ 3334 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3335 3336 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3337 """ 3338 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3339 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3340 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3341 target price value then broker opens a limit order. See also: `Order()` docstring. 3342 3343 :param lots: volume, integer count of lots >= 1. 3344 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3345 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3346 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3347 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3348 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3349 :param expDate: string "Undefined" by default or local date in future. 3350 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3351 This date is converting to UTC format for server. 3352 :return: JSON with response from broker server. 3353 """ 3354 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3355 3356 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3357 """ 3358 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3359 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3360 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3361 See also: `Order()` docstring. 3362 3363 :param lots: volume, integer count of lots >= 1. 3364 :param targetPrice: target price > 0. This is open trade price for limit order. 3365 :return: JSON with response from broker server. 3366 """ 3367 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3368 3369 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3370 """ 3371 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3372 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3373 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3374 target price value then broker opens a limit order. See also: `Order()` docstring. 3375 3376 :param lots: volume, integer count of lots >= 1. 3377 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3378 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3379 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3380 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3381 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3382 :param expDate: string "Undefined" by default or local date in future. 3383 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3384 This date is converting to UTC format for server. 3385 :return: JSON with response from broker server. 3386 """ 3387 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3388 3389 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3390 """ 3391 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3392 3393 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3394 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3395 This avoids unnecessary downloading data from the server. 3396 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3397 """ 3398 if self.accountId is None or not self.accountId: 3399 uLogger.error("Variable `accountId` must be defined for using this method!") 3400 raise Exception("Account ID required") 3401 3402 if orderIDs: 3403 if allOrdersIDs is None: 3404 rawOrders = self.RequestPendingOrders() 3405 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3406 3407 if allStopOrdersIDs is None: 3408 rawStopOrders = self.RequestStopOrders() 3409 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3410 3411 for orderID in orderIDs: 3412 idInPendingOrders = orderID in allOrdersIDs 3413 idInStopOrders = orderID in allStopOrdersIDs 3414 3415 if not (idInPendingOrders or idInStopOrders): 3416 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3417 continue 3418 3419 else: 3420 if idInPendingOrders: 3421 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3422 3423 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3424 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3425 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3426 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3427 3428 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3429 if self.moreDebug: 3430 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3431 3432 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3433 3434 else: 3435 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3436 3437 elif idInStopOrders: 3438 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3439 3440 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3441 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3442 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3443 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3444 3445 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3446 if self.moreDebug: 3447 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3448 3449 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3450 3451 else: 3452 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3453 3454 else: 3455 continue 3456 3457 def CloseAllOrders(self) -> None: 3458 """ 3459 Gets a list of open pending and stop orders and cancel it all. 3460 """ 3461 rawOrders = self.RequestPendingOrders() 3462 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3463 lenOrders = len(allOrdersIDs) 3464 3465 rawStopOrders = self.RequestStopOrders() 3466 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3467 lenSOrders = len(allStopOrdersIDs) 3468 3469 if lenOrders > 0 or lenSOrders > 0: 3470 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3471 3472 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3473 3474 else: 3475 uLogger.info("Orders not found, nothing to cancel.") 3476 3477 def CloseAll(self, *args) -> None: 3478 """ 3479 Close all available (not blocked) opened trades and orders. 3480 3481 Also, you can select one or more keywords case-insensitive: 3482 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3483 3484 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3485 """ 3486 overview = self.Overview(show=False) # get all open trades info 3487 3488 if len(args) == 0: 3489 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3490 self.CloseAllOrders() # close all pending and stop orders 3491 3492 for iType in TKS_INSTRUMENTS: 3493 if iType != "Currencies": 3494 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3495 3496 else: 3497 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3498 lowerArgs = [x.lower() for x in args] 3499 3500 if "orders" in lowerArgs: 3501 self.CloseAllOrders() # close all pending and stop orders 3502 3503 for iType in TKS_INSTRUMENTS: 3504 if iType.lower() in lowerArgs and iType != "Currencies": 3505 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3506 3507 def CloseAllByTicker(self, instrument: str) -> None: 3508 """ 3509 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3510 3511 This method searches opened trade and orders of instrument throw all portfolio and then use 3512 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3513 3514 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3515 3516 :param instrument: string with ticker. 3517 """ 3518 if instrument is None or not instrument: 3519 uLogger.error("Ticker name must be defined for using this method!") 3520 raise Exception("Ticker required") 3521 3522 overview = self.Overview(show=False) # get user portfolio with all open trades info 3523 3524 self._ticker = instrument # try to set instrument as ticker 3525 self._figi = "" 3526 3527 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3528 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3529 3530 if limitAll and self.IsInLimitOrders(portfolio=overview): 3531 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3532 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3533 3534 if stopAll and self.IsInStopOrders(portfolio=overview): 3535 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3536 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3537 3538 if self.IsInPortfolio(portfolio=overview): 3539 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3540 self.CloseTrades(instruments=[instrument], portfolio=overview) 3541 3542 def CloseAllByFIGI(self, instrument: str) -> None: 3543 """ 3544 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3545 3546 This method searches opened trade and orders of instrument throw all portfolio and then use 3547 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3548 3549 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3550 3551 :param instrument: string with FIGI id. 3552 """ 3553 if instrument is None or not instrument: 3554 uLogger.error("FIGI id must be defined for using this method!") 3555 raise Exception("FIGI required") 3556 3557 overview = self.Overview(show=False) # get user portfolio with all open trades info 3558 3559 self._ticker = "" 3560 self._figi = instrument # try to set instrument as FIGI id 3561 3562 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3563 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3564 3565 if limitAll and self.IsInLimitOrders(portfolio=overview): 3566 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3567 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3568 3569 if stopAll and self.IsInStopOrders(portfolio=overview): 3570 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3571 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3572 3573 if self.IsInPortfolio(portfolio=overview): 3574 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3575 self.CloseTrades(instruments=[instrument], portfolio=overview) 3576 3577 @staticmethod 3578 def ParseOrderParameters(operation, **inputParameters): 3579 """ 3580 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3581 3582 :param operation: string "Buy" or "Sell". 3583 :param inputParameters: this is dict of strings that looks like this 3584 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3585 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3586 "prices" key: one or more prices to open limit-orders 3587 Counts of values in lots and prices lists must be equals! 3588 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3589 """ 3590 # TODO: update order grid work with api v2 3591 pass 3592 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3593 # 3594 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3595 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3596 # raise Exception("Incorrect value") 3597 # 3598 # if "l" in inputParameters.keys(): 3599 # inputParameters["lots"] = inputParameters.pop("l") 3600 # 3601 # if "p" in inputParameters.keys(): 3602 # inputParameters["prices"] = inputParameters.pop("p") 3603 # 3604 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3605 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3606 # raise Exception("Incorrect value") 3607 # 3608 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3609 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3610 # 3611 # if len(lots) != len(prices): 3612 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3613 # raise Exception("Incorrect value") 3614 # 3615 # uLogger.debug("Extracted parameters for orders:") 3616 # uLogger.debug("lots = {}".format(lots)) 3617 # uLogger.debug("prices = {}".format(prices)) 3618 # 3619 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3620 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3621 # uLogger.debug("Order parameters: {}".format(result)) 3622 # 3623 # return result 3624 3625 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3626 """ 3627 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3628 3629 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3630 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3631 """ 3632 result = False 3633 msg = "Instrument not defined!" 3634 3635 if portfolio is None or not portfolio: 3636 portfolio = self.Overview(show=False) 3637 3638 if self._ticker: 3639 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3640 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3641 3642 for iType in TKS_INSTRUMENTS: 3643 for instrument in portfolio["stat"][iType]: 3644 if instrument["ticker"] == self._ticker: 3645 result = True 3646 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3647 break 3648 3649 elif self._figi: 3650 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3651 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3652 3653 for iType in TKS_INSTRUMENTS: 3654 for instrument in portfolio["stat"][iType]: 3655 if instrument["figi"] == self._figi: 3656 result = True 3657 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3658 break 3659 3660 else: 3661 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3662 3663 uLogger.debug(msg) 3664 3665 return result 3666 3667 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3668 """ 3669 Returns instrument from the user's portfolio if it presents there. 3670 Instrument must be defined by `ticker` (highly priority) or `figi`. 3671 3672 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3673 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3674 """ 3675 result = None 3676 msg = "Instrument not defined!" 3677 3678 if portfolio is None or not portfolio: 3679 portfolio = self.Overview(show=False) 3680 3681 if self._ticker: 3682 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3683 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3684 3685 for iType in TKS_INSTRUMENTS: 3686 for instrument in portfolio["stat"][iType]: 3687 if instrument["ticker"] == self._ticker: 3688 result = instrument 3689 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3690 break 3691 3692 elif self._figi: 3693 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3694 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3695 3696 for iType in TKS_INSTRUMENTS: 3697 for instrument in portfolio["stat"][iType]: 3698 if instrument["figi"] == self._figi: 3699 result = instrument 3700 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3701 break 3702 3703 else: 3704 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3705 3706 uLogger.debug(msg) 3707 3708 return result 3709 3710 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3711 """ 3712 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3713 3714 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3715 3716 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3717 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3718 """ 3719 result = False 3720 msg = "Instrument not defined!" 3721 3722 if portfolio is None or not portfolio: 3723 portfolio = self.Overview(show=False) 3724 3725 if self._ticker: 3726 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3727 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3728 3729 for instrument in portfolio["stat"]["orders"]: 3730 if instrument["ticker"] == self._ticker: 3731 result = True 3732 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3733 break 3734 3735 elif self._figi: 3736 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3737 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3738 3739 for instrument in portfolio["stat"]["orders"]: 3740 if instrument["figi"] == self._figi: 3741 result = True 3742 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3743 break 3744 3745 else: 3746 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3747 3748 uLogger.debug(msg) 3749 3750 return result 3751 3752 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3753 """ 3754 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3755 Instrument must be defined by `ticker` (highly priority) or `figi`. 3756 3757 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3758 3759 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3760 :return: list with `orderID`s of limit orders. 3761 """ 3762 result = [] 3763 msg = "Instrument not defined!" 3764 3765 if portfolio is None or not portfolio: 3766 portfolio = self.Overview(show=False) 3767 3768 if self._ticker: 3769 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3770 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3771 3772 for instrument in portfolio["stat"]["orders"]: 3773 if instrument["ticker"] == self._ticker: 3774 result.append(instrument["orderID"]) 3775 3776 if result: 3777 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3778 3779 elif self._figi: 3780 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3781 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3782 3783 for instrument in portfolio["stat"]["orders"]: 3784 if instrument["figi"] == self._figi: 3785 result.append(instrument["orderID"]) 3786 3787 if result: 3788 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3789 3790 else: 3791 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3792 3793 uLogger.debug(msg) 3794 3795 return result 3796 3797 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3798 """ 3799 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3800 3801 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3802 3803 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3804 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3805 """ 3806 result = False 3807 msg = "Instrument not defined!" 3808 3809 if portfolio is None or not portfolio: 3810 portfolio = self.Overview(show=False) 3811 3812 if self._ticker: 3813 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3814 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3815 3816 for instrument in portfolio["stat"]["stopOrders"]: 3817 if instrument["ticker"] == self._ticker: 3818 result = True 3819 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3820 break 3821 3822 elif self._figi: 3823 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3824 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3825 3826 for instrument in portfolio["stat"]["stopOrders"]: 3827 if instrument["figi"] == self._figi: 3828 result = True 3829 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3830 break 3831 3832 else: 3833 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3834 3835 uLogger.debug(msg) 3836 3837 return result 3838 3839 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3840 """ 3841 Returns list with all `orderID`s of opened stop orders for the instrument. 3842 Instrument must be defined by `ticker` (highly priority) or `figi`. 3843 3844 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3845 3846 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3847 :return: list with `orderID`s of stop orders. 3848 """ 3849 result = [] 3850 msg = "Instrument not defined!" 3851 3852 if portfolio is None or not portfolio: 3853 portfolio = self.Overview(show=False) 3854 3855 if self._ticker: 3856 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3857 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3858 3859 for instrument in portfolio["stat"]["stopOrders"]: 3860 if instrument["ticker"] == self._ticker: 3861 result.append(instrument["orderID"]) 3862 3863 if result: 3864 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3865 3866 elif self._figi: 3867 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3868 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3869 3870 for instrument in portfolio["stat"]["stopOrders"]: 3871 if instrument["figi"] == self._figi: 3872 result.append(instrument["orderID"]) 3873 3874 if result: 3875 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3876 3877 else: 3878 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3879 3880 uLogger.debug(msg) 3881 3882 return result 3883 3884 def RequestLimits(self) -> dict: 3885 """ 3886 Method for obtaining the available funds for withdrawal for current `accountId`. 3887 3888 See also: 3889 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3890 - `OverviewLimits()` method 3891 3892 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3893 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3894 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3895 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3896 """ 3897 if self.accountId is None or not self.accountId: 3898 uLogger.error("Variable `accountId` must be defined for using this method!") 3899 raise Exception("Account ID required") 3900 3901 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3902 3903 self.body = str({"accountId": self.accountId}) 3904 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3905 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3906 3907 if self.moreDebug: 3908 uLogger.debug("Records about available funds for withdrawal successfully received") 3909 3910 return rawLimits 3911 3912 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3913 """ 3914 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3915 3916 See also: `RequestLimits()`. 3917 3918 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3919 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3920 :return: dict with raw parsed data from server and some calculated statistics about it. 3921 """ 3922 if self.accountId is None or not self.accountId: 3923 uLogger.error("Variable `accountId` must be defined for using this method!") 3924 raise Exception("Account ID required") 3925 3926 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3927 3928 view = { 3929 "rawLimits": rawLimits, 3930 "limits": { # parsed data for every currency: 3931 "money": { # this is an array of portfolio currency positions 3932 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3933 }, 3934 "blocked": { # this is an array of blocked currency 3935 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3936 }, 3937 "blockedGuarantee": { # this is locked money under collateral for futures 3938 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3939 }, 3940 }, 3941 } 3942 3943 # --- Prepare text table with limits in human-readable format: 3944 if show or onlyFiles: 3945 info = [ 3946 "# Withdrawal limits\n\n", 3947 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3948 "* **Account ID:** [{}]\n".format(self.accountId), 3949 ] 3950 3951 if view["limits"]["money"]: 3952 info.extend([ 3953 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3954 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3955 ]) 3956 3957 else: 3958 info.append("\nNo withdrawal limits\n") 3959 3960 for curr in view["limits"]["money"].keys(): 3961 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3962 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3963 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3964 3965 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3966 "[{}]".format(curr), 3967 "{:.2f}".format(view["limits"]["money"][curr]), 3968 "{:.2f}".format(availableMoney), 3969 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3970 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3971 ) 3972 3973 if curr == "rub": 3974 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3975 3976 else: 3977 info.append(infoStr) 3978 3979 infoText = "".join(info) 3980 3981 if show and not onlyFiles: 3982 uLogger.info(infoText) 3983 3984 if self.withdrawalLimitsFile and (show or onlyFiles): 3985 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3986 fH.write(infoText) 3987 3988 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3989 3990 if self.useHTMLReports: 3991 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3992 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3993 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3994 3995 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3996 3997 return view 3998 3999 def RequestAccounts(self) -> dict: 4000 """ 4001 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4002 4003 See also: 4004 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4005 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4006 - `OverviewUserInfo()` method 4007 4008 :return: dict with raw data from server that contains accounts info. Example of dict: 4009 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4010 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4011 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4012 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4013 """ 4014 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4015 4016 self.body = str({}) 4017 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4018 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4019 4020 if self.moreDebug: 4021 uLogger.debug("Records about available accounts successfully received") 4022 4023 return rawAccounts 4024 4025 def RequestUserInfo(self) -> dict: 4026 """ 4027 Method for requesting common user's information. 4028 4029 See also: 4030 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4031 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4032 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4033 - `OverviewUserInfo()` method 4034 4035 :return: dict with raw data from server that contains user's information. Example of dict: 4036 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4037 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4038 """ 4039 uLogger.debug("Requesting common user's information. Wait, please...") 4040 4041 self.body = str({}) 4042 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4043 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4044 4045 if self.moreDebug: 4046 uLogger.debug("Records about current user successfully received") 4047 4048 return rawUserInfo 4049 4050 def RequestMarginStatus(self, accountId: str = None) -> dict: 4051 """ 4052 Method for requesting margin calculation for defined account ID. 4053 4054 See also: 4055 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4056 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4057 - `OverviewUserInfo()` method 4058 4059 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4060 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4061 Example of responses: 4062 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4063 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4064 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4065 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4066 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4067 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4068 """ 4069 if accountId is None or not accountId: 4070 if self.accountId is None or not self.accountId: 4071 uLogger.error("Variable `accountId` must be defined for using this method!") 4072 raise Exception("Account ID required") 4073 4074 else: 4075 accountId = self.accountId # use `self.accountId` (main ID) by default 4076 4077 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4078 4079 self.body = str({"accountId": accountId}) 4080 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4081 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4082 4083 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4084 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4085 rawMargin = {} 4086 4087 else: 4088 if self.moreDebug: 4089 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4090 4091 return rawMargin 4092 4093 def RequestTariffLimits(self) -> dict: 4094 """ 4095 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4096 4097 See also: 4098 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4099 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4100 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4101 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4102 - `OverviewUserInfo()` method 4103 4104 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4105 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4106 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4107 """ 4108 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4109 4110 self.body = str({}) 4111 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4112 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4113 4114 if self.moreDebug: 4115 uLogger.debug("Records with limits of current tariff successfully received") 4116 4117 return rawTariffLimits 4118 4119 def RequestBondCoupons(self, iJSON: dict) -> dict: 4120 """ 4121 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4122 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4123 All dates are in UTC timezone. 4124 4125 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4126 Documentation: 4127 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4128 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4129 4130 See also: `ExtendBondsData()`. 4131 4132 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4133 If raw iJSON is not data of bond then server returns an error [400] with message: 4134 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4135 :return: dictionary with bond payment calendar. Response example 4136 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4137 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4138 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4139 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4140 """ 4141 if iJSON["figi"] is None or not iJSON["figi"]: 4142 uLogger.error("FIGI must be defined for using this method!") 4143 raise Exception("FIGI required") 4144 4145 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4146 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4147 4148 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4149 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4150 self._figi, 4151 startDate, 4152 endDate, 4153 )) 4154 4155 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4156 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4157 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4158 4159 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4160 uLogger.warning("Instrument type is not bond!") 4161 4162 else: 4163 if self.moreDebug: 4164 uLogger.debug("Records about bond payment calendar successfully received") 4165 4166 return calendar 4167 4168 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4169 """ 4170 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4171 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4172 coupon yields, current yields and some statistics etc. 4173 4174 WARNING! This is too long operation if a lot of bonds requested from broker server. 4175 4176 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4177 4178 :param instruments: list of strings with tickers or FIGIs. 4179 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4180 for further used by data scientists or stock analytics. 4181 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4182 In XLSX-file and Pandas DataFrame fields mean: 4183 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4184 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4185 """ 4186 if instruments is None or not instruments: 4187 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4188 raise Exception("Ticker or FIGI required") 4189 4190 if isinstance(instruments, str): 4191 instruments = [instruments] 4192 4193 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4194 4195 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4196 4197 iCount = len(uniqueInstruments) 4198 tooLong = iCount >= 20 4199 if tooLong: 4200 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4201 4202 bonds = None 4203 for i, self._figi in enumerate(uniqueInstruments): 4204 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4205 4206 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4207 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4208 rawBond = self.SearchByFIGI(requestPrice=True) 4209 4210 # Widen raw data with UTC current time (iData["actualDateTime"]): 4211 actualDate = datetime.now(tzutc()) 4212 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4213 4214 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4215 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4216 4217 # Replace some values with human-readable: 4218 iData["nominalCurrency"] = iData["nominal"]["currency"] 4219 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4220 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4221 iData["aciCurrency"] = iData["aciValue"]["currency"] 4222 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4223 iData["issueSize"] = int(iData["issueSize"]) 4224 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4225 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4226 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4227 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4228 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4229 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4230 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4231 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4232 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4233 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4234 4235 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4236 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4237 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4238 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4239 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4240 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4241 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4242 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4243 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4244 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4245 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4246 4247 # Widen raw data with calendar data from `rawCalendar` values: 4248 calendarData = [] 4249 if "events" in iData["rawCalendar"].keys(): 4250 for item in iData["rawCalendar"]["events"]: 4251 calendarData.append({ 4252 "couponDate": item["couponDate"], 4253 "couponNumber": int(item["couponNumber"]), 4254 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4255 "payCurrency": item["payOneBond"]["currency"], 4256 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4257 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4258 "couponStartDate": item["couponStartDate"], 4259 "couponEndDate": item["couponEndDate"], 4260 "couponPeriod": item["couponPeriod"], 4261 }) 4262 4263 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4264 if "maturityDate" not in iData.keys(): 4265 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4266 4267 # Widen raw data with Coupon Rate. 4268 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4269 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4270 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4271 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4272 4273 # Widen raw data with Yield to Maturity (YTM) on current date. 4274 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4275 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4276 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4277 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4278 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4279 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4280 4281 iData["calendar"] = calendarData # adds calendar at the end 4282 4283 # Remove not used data: 4284 iData.pop("uid") 4285 iData.pop("positionUid") 4286 iData.pop("currentPrice") 4287 iData.pop("rawCalendar") 4288 4289 colNames = list(iData.keys()) 4290 if bonds is None: 4291 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4292 4293 else: 4294 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4295 4296 else: 4297 uLogger.warning("Instrument is not a bond!") 4298 4299 processed = round(100 * (i + 1) / iCount, 1) 4300 if tooLong and processed % 5 == 0: 4301 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4302 4303 else: 4304 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4305 4306 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4307 4308 # Saving bonds from Pandas DataFrame to XLSX sheet: 4309 if xlsx and self.bondsXLSXFile: 4310 with pd.ExcelWriter( 4311 path=self.bondsXLSXFile, 4312 date_format=TKS_DATE_FORMAT, 4313 datetime_format=TKS_DATE_TIME_FORMAT, 4314 mode="w", 4315 ) as writer: 4316 bonds.to_excel( 4317 writer, 4318 sheet_name="Extended bonds data", 4319 index=True, 4320 encoding="UTF-8", 4321 freeze_panes=(1, 1), 4322 ) # saving as XLSX-file with freeze first row and column as headers 4323 4324 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4325 4326 return bonds 4327 4328 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4329 """ 4330 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4331 4332 WARNING! This is too long operation if a lot of bonds requested from broker server. 4333 4334 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4335 4336 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4337 extended information about bonds: main info, current prices, bond payment calendar, 4338 coupon yields, current yields and some statistics etc. 4339 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4340 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4341 for further used by data scientists or stock analytics. 4342 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4343 """ 4344 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4345 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4346 4347 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4348 4349 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4350 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4351 calendar = None 4352 for bond in extBonds.iterrows(): 4353 for item in bond[1]["calendar"]: 4354 cData = { 4355 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4356 "couponDate": item["couponDate"], 4357 "figi": bond[1]["figi"], 4358 "ticker": bond[1]["ticker"], 4359 "name": bond[1]["name"], 4360 "couponNumber": item["couponNumber"], 4361 "payOneBond": item["payOneBond"], 4362 "payCurrency": item["payCurrency"], 4363 "couponType": item["couponType"], 4364 "couponPeriod": item["couponPeriod"], 4365 "fixDate": item["fixDate"], 4366 "couponStartDate": item["couponStartDate"], 4367 "couponEndDate": item["couponEndDate"], 4368 } 4369 4370 if calendar is None: 4371 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4372 4373 else: 4374 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4375 4376 if calendar is not None: 4377 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4378 4379 # Saving calendar from Pandas DataFrame to XLSX sheet: 4380 if xlsx: 4381 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4382 4383 with pd.ExcelWriter( 4384 path=xlsxCalendarFile, 4385 date_format=TKS_DATE_FORMAT, 4386 datetime_format=TKS_DATE_TIME_FORMAT, 4387 mode="w", 4388 ) as writer: 4389 humanReadable = calendar.copy(deep=True) 4390 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4391 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4392 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4393 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4394 humanReadable.columns = colNames # human-readable column names 4395 4396 humanReadable.to_excel( 4397 writer, 4398 sheet_name="Bond payments calendar", 4399 index=False, 4400 encoding="UTF-8", 4401 freeze_panes=(1, 2), 4402 ) # saving as XLSX-file with freeze first row and column as headers 4403 4404 del humanReadable # release df in memory 4405 4406 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4407 4408 return calendar 4409 4410 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4411 """ 4412 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4413 Also, creates Markdown file with calendar data, `calendar.md` by default. 4414 4415 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4416 4417 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4418 extended information about bonds: main info, current prices, bond payment calendar, 4419 coupon yields, current yields and some statistics etc. 4420 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4421 :param show: if `True` then also printing bonds payment calendar to the console, 4422 otherwise save to file `calendarFile` only. `False` by default. 4423 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4424 :return: multilines text in Markdown format with bonds payment calendar as a table. 4425 """ 4426 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4427 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4428 4429 infoText = "# Bond payments calendar\n\n" 4430 4431 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4432 4433 if not (calendar is None or calendar.empty): 4434 splitLine = "| | | | | | | | | |\n" 4435 4436 info = [ 4437 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4438 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4439 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4440 ] 4441 4442 newMonth = False 4443 notOneBond = calendar["figi"].nunique() > 1 4444 for i, bond in enumerate(calendar.iterrows()): 4445 if newMonth and notOneBond: 4446 info.append(splitLine) 4447 4448 info.append( 4449 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4450 " √" if bond[1]["paid"] else " —", 4451 bond[1]["couponDate"].split("T")[0], 4452 bond[1]["figi"], 4453 bond[1]["ticker"], 4454 bond[1]["couponNumber"], 4455 "{} {}".format( 4456 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4457 bond[1]["payCurrency"], 4458 ), 4459 bond[1]["couponType"], 4460 bond[1]["couponPeriod"], 4461 bond[1]["fixDate"].split("T")[0], 4462 ) 4463 ) 4464 4465 if i < len(calendar.values) - 1: 4466 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4467 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4468 newMonth = False if curDate.month == nextDate.month else True 4469 4470 else: 4471 newMonth = False 4472 4473 infoText += "".join(info) 4474 4475 if show and not onlyFiles: 4476 uLogger.info("{}".format(infoText)) 4477 4478 if self.calendarFile is not None and (show or onlyFiles): 4479 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4480 fH.write(infoText) 4481 4482 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4483 4484 if self.useHTMLReports: 4485 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4486 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4487 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4488 4489 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4490 4491 else: 4492 infoText += "No data\n" 4493 4494 return infoText 4495 4496 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4497 """ 4498 Method for parsing and show simple table with all available user accounts. 4499 4500 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4501 4502 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4503 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4504 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4505 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4506 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4507 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4508 "closed": "—", "access": "Full access" }, ...}}` 4509 """ 4510 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4511 4512 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4513 accounts = { 4514 item["id"]: { 4515 "type": TKS_ACCOUNT_TYPES[item["type"]], 4516 "name": item["name"], 4517 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4518 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4519 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4520 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4521 } for item in rawAccounts["accounts"] 4522 } 4523 4524 # Raw and parsed data with some fields replaced in "stat" section: 4525 view = { 4526 "rawAccounts": rawAccounts, 4527 "stat": accounts, 4528 } 4529 4530 # --- Prepare simple text table with only accounts data in human-readable format: 4531 if show or onlyFiles: 4532 info = [ 4533 "# User accounts\n\n", 4534 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4535 "| Account ID | Type | Status | Name |\n", 4536 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4537 ] 4538 4539 for account in view["stat"].keys(): 4540 info.extend([ 4541 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4542 account, 4543 view["stat"][account]["type"], 4544 view["stat"][account]["status"], 4545 view["stat"][account]["name"], 4546 ) 4547 ]) 4548 4549 infoText = "".join(info) 4550 4551 if show and not onlyFiles: 4552 uLogger.info(infoText) 4553 4554 if self.userAccountsFile and (show or onlyFiles): 4555 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4556 fH.write(infoText) 4557 4558 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4559 4560 if self.useHTMLReports: 4561 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4562 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4563 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4564 4565 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4566 4567 return view 4568 4569 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4570 """ 4571 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4572 4573 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4574 4575 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4576 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4577 :return: dict with raw parsed data from server and some calculated statistics about it. 4578 """ 4579 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4580 tmpTicker = self._ticker 4581 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4582 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4583 self._ticker = tmpTicker 4584 4585 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4586 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4587 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4588 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4589 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4590 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4591 4592 # This is dict with parsed common user data: 4593 userInfo = { 4594 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4595 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4596 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4597 "tariff": rawUserInfo["tariff"], 4598 } 4599 4600 # This is an array of dict with parsed margin statuses for every account IDs: 4601 margins = {} 4602 for accountId in accounts.keys(): 4603 if rawMargins[accountId]: 4604 margins[accountId] = { 4605 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4606 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4607 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4608 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4609 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4610 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4611 "missing": missing["volume"], 4612 } 4613 4614 else: 4615 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4616 4617 unary = {} # unary-connection limits 4618 for item in rawTariffLimits["unaryLimits"]: 4619 if item["limitPerMinute"] in unary.keys(): 4620 unary[item["limitPerMinute"]].extend(item["methods"]) 4621 4622 else: 4623 unary[item["limitPerMinute"]] = item["methods"] 4624 4625 stream = {} # stream-connection limits 4626 for item in rawTariffLimits["streamLimits"]: 4627 if item["limit"] in stream.keys(): 4628 stream[item["limit"]].extend(item["streams"]) 4629 4630 else: 4631 stream[item["limit"]] = item["streams"] 4632 4633 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4634 limits = { 4635 "unary": unary, 4636 "stream": stream, 4637 } 4638 4639 # Raw and parsed data as an output result: 4640 view = { 4641 "rawUserInfo": rawUserInfo, 4642 "rawAccounts": rawAccounts, 4643 "rawMargins": rawMargins, 4644 "rawTariffLimits": rawTariffLimits, 4645 "stat": { 4646 "overview": overview, 4647 "userInfo": userInfo, 4648 "accounts": accounts, 4649 "margins": margins, 4650 "limits": limits, 4651 }, 4652 } 4653 4654 # --- Prepare text table with user information in human-readable format: 4655 if show or onlyFiles: 4656 info = [ 4657 "# Full user information\n\n", 4658 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4659 "## Common information\n\n", 4660 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4661 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4662 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4663 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4664 "\n## User accounts\n\n", 4665 ] 4666 4667 for account in view["stat"]["accounts"].keys(): 4668 info.extend([ 4669 "### ID: [{}]\n\n".format(account), 4670 "| Parameters | Values |\n", 4671 "|----------------------|--------------------------------------------------------------|\n", 4672 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4673 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4674 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4675 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4676 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4677 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4678 ]) 4679 4680 if margins[account]: 4681 info.extend([ 4682 "| Margin status: | Enabled |\n", 4683 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4684 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4685 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4686 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4687 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4688 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4689 ]) 4690 4691 else: 4692 info.append("| Margin status: | Disabled |\n\n") 4693 4694 info.extend([ 4695 "\n## Current user tariff limits\n", 4696 "\n### See also\n", 4697 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4698 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4699 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4700 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4701 "\n### Unary limits\n", 4702 ]) 4703 4704 if unary: 4705 for key, values in sorted(unary.items()): 4706 info.append("\n* Max requests per minute: {}\n".format(key)) 4707 4708 for value in values: 4709 info.append(" - {}\n".format(value)) 4710 4711 else: 4712 info.append("\nNot available\n") 4713 4714 info.append("\n### Stream limits\n") 4715 4716 if stream: 4717 for key, values in sorted(stream.items()): 4718 info.append("\n* Max stream connections: {}\n".format(key)) 4719 4720 for value in values: 4721 info.append(" - {}\n".format(value)) 4722 4723 else: 4724 info.append("\nNot available\n") 4725 4726 infoText = "".join(info) 4727 4728 if show and not onlyFiles: 4729 uLogger.info(infoText) 4730 4731 if self.userInfoFile and (show or onlyFiles): 4732 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4733 fH.write(infoText) 4734 4735 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4736 4737 if self.useHTMLReports: 4738 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4739 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4740 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4741 4742 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4743 4744 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.__lock = Lock() # initialize multiprocessing mutex lock 130 131 self.aliases = TKS_TICKER_ALIASES 132 """Some aliases instead official tickers. 133 134 See also: `TKSEnums.TKS_TICKER_ALIASES` 135 """ 136 137 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 138 139 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 140 141 self._ticker = "" 142 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 143 144 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 145 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 146 147 See also: `SearchByTicker()`, `SearchInstruments()`. 148 """ 149 150 self._figi = "" 151 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 152 153 See also: `SearchByFIGI()`, `SearchInstruments()`. 154 """ 155 156 self.depth = 1 157 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 158 159 See also: `GetCurrentPrices()`. 160 """ 161 162 self.server = r"https://invest-public-api.tinkoff.ru/rest" 163 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 164 165 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 166 """ 167 168 uLogger.debug("Broker API server: {}".format(self.server)) 169 170 self.timeout = 15 171 """Server operations timeout in seconds. Default: `15`. 172 173 See also: `SendAPIRequest()`. 174 """ 175 176 self.headers = { 177 "Content-Type": "application/json", 178 "accept": "application/json", 179 "Authorization": "Bearer {}".format(self.token), 180 "x-app-name": "Tim55667757.TKSBrokerAPI", 181 } 182 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 183 184 See also: `SendAPIRequest()`. 185 """ 186 187 self.body = None 188 """Request body which send to broker server. Default: `None`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.moreDebug = False 194 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 195 196 self.useHTMLReports = False 197 """ 198 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 199 200 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 201 """ 202 203 self.historyFile = None 204 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 205 206 See also: `History()`. 207 """ 208 209 self.htmlHistoryFile = "index.html" 210 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 211 212 See also: `ShowHistoryChart()`. 213 """ 214 215 self.instrumentsFile = "instruments.md" 216 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 217 218 See also: `ShowInstrumentsInfo()`. 219 """ 220 221 self.searchResultsFile = "search-results.md" 222 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 223 224 See also: `SearchInstruments()`. 225 """ 226 227 self.pricesFile = "prices.md" 228 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 229 230 See also: `GetListOfPrices()`. 231 """ 232 233 self.infoFile = "info.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 237 """ 238 239 self.bondsXLSXFile = "ext-bonds.xlsx" 240 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 241 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 242 243 See also: `ExtendBondsData()`. 244 """ 245 246 self.calendarFile = "calendar.md" 247 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 248 249 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 250 251 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 252 """ 253 254 self.overviewFile = "overview.md" 255 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 256 257 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 258 """ 259 260 self.overviewDigestFile = "overview-digest.md" 261 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 262 263 See also: `Overview()` with parameter `details="digest"`. 264 """ 265 266 self.overviewPositionsFile = "overview-positions.md" 267 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 268 269 See also: `Overview()` with parameter `details="positions"`. 270 """ 271 272 self.overviewOrdersFile = "overview-orders.md" 273 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 274 275 See also: `Overview()` with parameter `details="orders"`. 276 """ 277 278 self.overviewAnalyticsFile = "overview-analytics.md" 279 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 280 281 See also: `Overview()` with parameter `details="analytics"`. 282 """ 283 284 self.overviewBondsCalendarFile = "overview-calendar.md" 285 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 286 287 See also: `Overview()` with parameter `details="calendar"`. 288 """ 289 290 self.reportFile = "deals.md" 291 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 292 293 See also: `Deals()`. 294 """ 295 296 self.withdrawalLimitsFile = "limits.md" 297 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 298 299 See also: `OverviewLimits()` and `RequestLimits()`. 300 """ 301 302 self.userInfoFile = "user-info.md" 303 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 304 305 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 306 """ 307 308 self.userAccountsFile = "accounts.md" 309 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 310 311 See also: `OverviewAccounts()`, `RequestAccounts()`. 312 """ 313 314 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 315 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 316 317 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 318 319 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 320 """ 321 322 self.iList = None # init iList for raw instruments data 323 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 324 325 See also: `Listing()`, `DumpInstruments()`. 326 """ 327 328 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 329 if useCache: 330 if os.path.exists(self.iListDumpFile): 331 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 332 curTime = datetime.now(tzutc()) 333 334 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 335 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 336 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 338 339 else: 340 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 341 342 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 343 os.path.abspath(self.iListDumpFile), 344 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 345 )) 346 347 else: 348 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 349 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 350 351 else: 352 self.iList = self.Listing() # request new raw instruments data from broker server 353 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 354 355 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 356 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 357 358 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 359 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.
See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
413 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 414 """ 415 Send GET or POST request to broker server and receive JSON object. 416 417 self.header: must be defining with dictionary of headers. 418 self.body: if define then used as request body. None by default. 419 self.timeout: global request timeout, 15 seconds by default. 420 :param url: url with REST request. 421 :param reqType: send "GET" or "POST" request. "GET" by default. 422 :param retry: how many times retry after first request if an 5xx server errors occurred. 423 :param pause: sleep time in seconds between retries. 424 :return: response JSON (dictionary) from broker. 425 """ 426 if reqType.upper() not in ("GET", "POST"): 427 uLogger.error("You can define request type: `GET` or `POST`!") 428 raise Exception("Incorrect value") 429 430 if self.moreDebug: 431 uLogger.debug("Request parameters:") 432 uLogger.debug(" - REST API URL: {}".format(url)) 433 uLogger.debug(" - request type: {}".format(reqType)) 434 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 435 uLogger.debug(" - body:\n{}".format(self.body)) 436 437 # fast hack to avoid all operations with some tickers/FIGI 438 responseJSON = {} 439 oK = True 440 for item in self.exclude: 441 if item in url: 442 if self.moreDebug: 443 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 444 445 oK = False 446 break 447 448 if oK: 449 with self.__lock: # acquire the mutex lock 450 counter = 0 451 response = None 452 errMsg = "" 453 454 while not response and counter <= retry: 455 if reqType == "GET": 456 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 457 458 if reqType == "POST": 459 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 460 461 if self.moreDebug: 462 uLogger.debug("Response:") 463 uLogger.debug(" - status code: {}".format(response.status_code)) 464 uLogger.debug(" - reason: {}".format(response.reason)) 465 uLogger.debug(" - body length: {}".format(len(response.text))) 466 uLogger.debug(" - headers:\n{}".format(response.headers)) 467 468 # Server returns some headers: 469 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 470 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 471 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 472 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 473 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 474 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 475 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 476 sleep(rateLimitWait) 477 478 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 479 if 400 <= response.status_code < 500: 480 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 481 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 482 483 if "code" in response.text and "message" in response.text: 484 msgDict = self._ParseJSON(rawData=response.text) 485 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 486 487 counter = retry + 1 # do not retry for 4xx errors 488 489 if 500 <= response.status_code < 600: 490 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 491 uLogger.debug(" - not oK, {}".format(errMsg)) 492 493 if "code" in response.text and "message" in response.text: 494 errMsgDict = self._ParseJSON(rawData=response.text) 495 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 496 497 counter += 1 498 499 if counter <= retry: 500 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 501 sleep(pause) 502 503 responseJSON = self._ParseJSON(rawData=response.text) 504 505 if errMsg: 506 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 507 uLogger.error(" - not oK, {}".format(errMsg)) 508 509 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
542 def Listing(self) -> dict: 543 """ 544 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 545 546 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 547 """ 548 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 549 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 550 551 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 552 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 553 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 554 555 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 556 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 557 poolUpdater.close() # close the thread pool 558 poolUpdater.join() # wait a moment until all data returns from threads 559 560 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 561 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 562 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 563 564 # calculate minimum price increment (step) for all instruments and set up instrument's type: 565 for iType in iList.keys(): 566 for ticker in iList[iType]: 567 iList[iType][ticker]["type"] = iType 568 569 if "minPriceIncrement" in iList[iType][ticker].keys(): 570 iList[iType][ticker]["step"] = NanoToFloat( 571 iList[iType][ticker]["minPriceIncrement"]["units"], 572 iList[iType][ticker]["minPriceIncrement"]["nano"], 573 ) 574 575 else: 576 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 577 578 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
580 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 581 """ 582 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 583 584 See also: `DumpInstruments()`, `Listing()`. 585 586 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 587 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 588 """ 589 if self.iListDumpFile is None or not self.iListDumpFile: 590 uLogger.error("Output name of dump file must be defined!") 591 raise Exception("Filename required") 592 593 if not self.iList or forceUpdate: 594 self.iList = self.Listing() 595 596 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 597 598 # Save as XLSX with separated sheets for every type of instruments: 599 with pd.ExcelWriter( 600 path=xlsxDumpFile, 601 date_format=TKS_DATE_FORMAT, 602 datetime_format=TKS_DATE_TIME_FORMAT, 603 mode="w", 604 ) as writer: 605 for iType in TKS_INSTRUMENTS: 606 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 607 df = df[sorted(df)] # sorted by column names 608 df = df.applymap( 609 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 610 na_action="ignore", 611 ) # converting numbers from nano-type to float in every cell 612 df.to_excel( 613 writer, 614 sheet_name=iType, 615 encoding="UTF-8", 616 freeze_panes=(1, 1), 617 ) # saving as XLSX-file with freeze first row and column as headers 618 619 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
621 def DumpInstruments(self, forceUpdate: bool = True) -> str: 622 """ 623 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 624 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 625 626 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 627 628 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 629 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 630 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 631 """ 632 if self.iListDumpFile is None or not self.iListDumpFile: 633 uLogger.error("Output name of dump file must be defined!") 634 raise Exception("Filename required") 635 636 if not self.iList or forceUpdate: 637 self.iList = self.Listing() 638 639 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 640 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 641 fH.write(jsonDump) 642 643 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 644 645 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
647 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 648 """ 649 Show information about one instrument defined by json data and prints it in Markdown format. 650 651 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 652 653 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 654 :param show: if `True` then also printing information about instrument and its current price. 655 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 656 :return: multilines text in Markdown format with information about one instrument. 657 """ 658 splitLine = "| | |\n" 659 infoText = "" 660 661 if iJSON is not None and iJSON and isinstance(iJSON, dict): 662 info = [ 663 "# Main information\n\n", 664 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 665 "| Parameters | Values |\n", 666 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 667 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 668 "| Full name: | {:<54} |\n".format(iJSON["name"]), 669 ] 670 671 if "sector" in iJSON.keys() and iJSON["sector"]: 672 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 673 674 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 675 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 676 677 info.extend([ 678 splitLine, 679 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 680 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 681 ]) 682 683 if "isin" in iJSON.keys() and iJSON["isin"]: 684 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 685 686 if "classCode" in iJSON.keys(): 687 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 688 689 info.extend([ 690 splitLine, 691 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 692 splitLine, 693 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 694 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 695 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 696 ]) 697 698 if iJSON["figi"]: 699 self._figi = iJSON["figi"] 700 iJSON = iJSON | self.RequestTradingStatus() 701 702 info.extend([ 703 splitLine, 704 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 705 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 706 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 707 ]) 708 709 info.append(splitLine) 710 711 if "type" in iJSON.keys() and iJSON["type"]: 712 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 713 714 if "shareType" in iJSON.keys() and iJSON["shareType"]: 715 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 716 717 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 718 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 719 720 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 721 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 722 723 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 724 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 725 726 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 727 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 728 729 if "focusType" in iJSON.keys() and iJSON["focusType"]: 730 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 731 732 if "assetType" in iJSON.keys() and iJSON["assetType"]: 733 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 734 735 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 736 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 737 738 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 739 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 740 741 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 742 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 743 744 if "currency" in iJSON.keys(): 745 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 746 747 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 748 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 749 750 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 751 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 752 753 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 754 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 755 756 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 757 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 758 759 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 760 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 761 762 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 763 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 764 765 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 766 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 767 768 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 769 info.append("| Perpetual bond: | Yes |\n") 770 771 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 772 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 773 774 iExt = None 775 if iJSON["type"] == "Bonds": 776 info.extend([ 777 splitLine, 778 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 779 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 780 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 781 iJSON["nominal"]["currency"], 782 )), 783 ]) 784 785 if "floatingCouponFlag" in iJSON.keys(): 786 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 787 788 if "amortizationFlag" in iJSON.keys(): 789 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 790 791 info.append(splitLine) 792 793 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 794 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 795 796 if iJSON["figi"]: 797 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 798 799 info.extend([ 800 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 801 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 802 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 803 ]) 804 805 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 806 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 807 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 808 iJSON["aciValue"]["currency"] 809 ))) 810 811 if "currentPrice" in iJSON.keys(): 812 info.append(splitLine) 813 814 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 815 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 816 817 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 818 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 819 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 820 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 821 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 822 823 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 824 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 825 826 info.extend([ 827 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 828 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 829 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 830 )), 831 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 832 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 833 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 834 )), 835 "| Changes between last deal price and last close | {:<54} |\n".format( 836 "{:.2f}%{}".format( 837 iJSON["currentPrice"]["changes"], 838 " ({}{:.2f} {})".format( 839 "+" if bondChangesDelta > 0 else "", 840 bondChangesDelta, 841 aciCurrency 842 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 843 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 844 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 845 currency 846 ), 847 ) 848 ), 849 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 850 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 851 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 852 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 853 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 854 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 855 )), 856 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 857 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 858 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 859 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 860 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 861 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 862 )), 863 ]) 864 865 if "lot" in iJSON.keys(): 866 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 867 868 if "step" in iJSON.keys() and iJSON["step"] != 0: 869 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 870 871 # Add bond payment calendar: 872 if iJSON["type"] == "Bonds": 873 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 874 info.extend(["\n#", strCalendar]) 875 876 infoText += "".join(info) 877 878 if show and not onlyFiles: 879 uLogger.info("{}".format(infoText)) 880 881 if self.infoFile is not None and (show or onlyFiles): 882 with open(self.infoFile, "w", encoding="UTF-8") as fH: 883 fH.write(infoText) 884 885 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 886 887 if self.useHTMLReports: 888 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 889 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 890 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 891 892 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 893 894 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self._ticker] - show: if
Truethen also printing information about instrument and its current price. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format with information about one instrument.
896 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 897 """ 898 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 899 900 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 901 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 902 :return: JSON formatted data with information about instrument. 903 """ 904 tickerJSON = {} 905 if self.moreDebug: 906 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 907 908 if not self._ticker: 909 uLogger.warning("self._ticker variable is not be empty!") 910 911 else: 912 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 913 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 914 raise Exception("Instrument not allowed") 915 916 if not self.iList: 917 self.iList = self.Listing() 918 919 if self._ticker in self.iList["Shares"].keys(): 920 tickerJSON = self.iList["Shares"][self._ticker] 921 if self.moreDebug: 922 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 923 924 elif self._ticker in self.iList["Currencies"].keys(): 925 tickerJSON = self.iList["Currencies"][self._ticker] 926 if self.moreDebug: 927 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 928 929 elif self._ticker in self.iList["Bonds"].keys(): 930 tickerJSON = self.iList["Bonds"][self._ticker] 931 if self.moreDebug: 932 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 933 934 elif self._ticker in self.iList["Etfs"].keys(): 935 tickerJSON = self.iList["Etfs"][self._ticker] 936 if self.moreDebug: 937 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 938 939 elif self._ticker in self.iList["Futures"].keys(): 940 tickerJSON = self.iList["Futures"][self._ticker] 941 if self.moreDebug: 942 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 943 944 if tickerJSON: 945 self._figi = tickerJSON["figi"] 946 947 if requestPrice: 948 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 949 950 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 951 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 952 953 else: 954 tickerJSON["currentPrice"]["changes"] = 0 955 956 if show: 957 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 958 959 else: 960 if show: 961 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 962 963 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
965 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 966 """ 967 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 968 969 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 970 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 971 :return: JSON formatted data with information about instrument. 972 """ 973 figiJSON = {} 974 if self.moreDebug: 975 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 976 977 if not self._figi: 978 uLogger.warning("self._figi variable is not be empty!") 979 980 else: 981 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 982 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 983 raise Exception("Instrument not allowed") 984 985 if not self.iList: 986 self.iList = self.Listing() 987 988 for item in self.iList["Shares"].keys(): 989 if self._figi == self.iList["Shares"][item]["figi"]: 990 figiJSON = self.iList["Shares"][item] 991 992 if self.moreDebug: 993 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 994 995 break 996 997 if not figiJSON: 998 for item in self.iList["Currencies"].keys(): 999 if self._figi == self.iList["Currencies"][item]["figi"]: 1000 figiJSON = self.iList["Currencies"][item] 1001 1002 if self.moreDebug: 1003 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1004 1005 break 1006 1007 if not figiJSON: 1008 for item in self.iList["Bonds"].keys(): 1009 if self._figi == self.iList["Bonds"][item]["figi"]: 1010 figiJSON = self.iList["Bonds"][item] 1011 1012 if self.moreDebug: 1013 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1014 1015 break 1016 1017 if not figiJSON: 1018 for item in self.iList["Etfs"].keys(): 1019 if self._figi == self.iList["Etfs"][item]["figi"]: 1020 figiJSON = self.iList["Etfs"][item] 1021 1022 if self.moreDebug: 1023 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1024 1025 break 1026 1027 if not figiJSON: 1028 for item in self.iList["Futures"].keys(): 1029 if self._figi == self.iList["Futures"][item]["figi"]: 1030 figiJSON = self.iList["Futures"][item] 1031 1032 if self.moreDebug: 1033 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1034 1035 break 1036 1037 if figiJSON: 1038 self._figi = figiJSON["figi"] 1039 self._ticker = figiJSON["ticker"] 1040 1041 if requestPrice: 1042 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1043 1044 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1045 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1046 1047 else: 1048 figiJSON["currentPrice"]["changes"] = 0 1049 1050 if show: 1051 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1052 1053 else: 1054 if show: 1055 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1056 1057 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1059 def GetCurrentPrices(self, show: bool = True) -> dict: 1060 """ 1061 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1062 `{"buy": [{"price": 1243.8, "quantity": 193}, 1063 {"price": 1244.0, "quantity": 168}, 1064 {"price": 1244.8, "quantity": 5}, 1065 {"price": 1245.0, "quantity": 61}, 1066 {"price": 1245.4, "quantity": 60}], 1067 "sell": [{"price": 1243.6, "quantity": 8}, 1068 {"price": 1242.6, "quantity": 10}, 1069 {"price": 1242.4, "quantity": 18}, 1070 {"price": 1242.2, "quantity": 50}, 1071 {"price": 1242.0, "quantity": 113}], 1072 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1073 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1074 - sell: list of dicts with Buyers prices, 1075 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1076 - quantity: volume value by current price in lots, 1077 - limitUp: current trade session limit price, maximum, 1078 - limitDown: current trade session limit price, minimum, 1079 - lastPrice: last deal price of the instrument, 1080 - closePrice: previous trade session close price of the instrument. 1081 1082 See also: `SearchByTicker()` and `SearchByFIGI()`. 1083 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1084 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1085 1086 :param show: if `True` then print DOM to log and console. 1087 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1088 If an error occurred then returns an empty record: 1089 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1090 """ 1091 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1092 1093 if self.depth < 1: 1094 uLogger.error("Depth of Market (DOM) must be >=1!") 1095 raise Exception("Incorrect value") 1096 1097 if not (self._ticker or self._figi): 1098 uLogger.error("self._ticker or self._figi variables must be defined!") 1099 raise Exception("Ticker or FIGI required") 1100 1101 if self._ticker and not self._figi: 1102 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1103 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1104 1105 if not self._ticker and self._figi: 1106 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1107 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1108 1109 if not self._figi: 1110 uLogger.error("FIGI is not defined!") 1111 raise Exception("Ticker or FIGI required") 1112 1113 else: 1114 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1115 1116 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1117 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1118 self.body = str({"figi": self._figi, "depth": self.depth}) 1119 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1120 1121 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1122 # list of dicts with sellers orders: 1123 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1124 1125 # list of dicts with buyers orders: 1126 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1127 1128 # max price of instrument at this time: 1129 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1130 1131 # min price of instrument at this time: 1132 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1133 1134 # last price of deal with instrument: 1135 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1136 1137 # last close price of instrument: 1138 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1139 1140 else: 1141 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1142 uLogger.debug("Server response: {}".format(pricesResponse)) 1143 1144 if show: 1145 if prices["buy"] or prices["sell"]: 1146 info = [ 1147 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1148 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1149 self._ticker, 1150 self._figi, 1151 self.depth, 1152 ), 1153 "-" * 60, "\n", 1154 " Orders of Buyers | Orders of Sellers\n", 1155 "-" * 60, "\n", 1156 " Sell prices (volumes) | Buy prices (volumes)\n", 1157 "-" * 60, "\n", 1158 ] 1159 1160 if not prices["buy"]: 1161 info.append(" | No orders!\n") 1162 sumBuy = 0 1163 1164 else: 1165 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1166 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1167 for item in maxMinSorted: 1168 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1169 1170 if not prices["sell"]: 1171 info.append("No orders! |\n") 1172 sumSell = 0 1173 1174 else: 1175 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1176 for item in prices["sell"]: 1177 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1178 1179 info.extend([ 1180 "-" * 60, "\n", 1181 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1182 "-" * 60, "\n", 1183 ]) 1184 1185 infoText = "".join(info) 1186 1187 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1188 1189 else: 1190 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1191 1192 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1194 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1195 """ 1196 This method get and show information about all available broker instruments for current user account. 1197 If `instrumentsFile` string is not empty then also save information to this file. 1198 1199 :param show: if `True` then print results to console, if `False` — print only to file. 1200 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1201 :return: multi-lines string with all available broker instruments. 1202 """ 1203 if not self.iList: 1204 self.iList = self.Listing() 1205 1206 info = [ 1207 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1208 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1209 ] 1210 1211 # add instruments count by type: 1212 for iType in self.iList.keys(): 1213 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1214 1215 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1216 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1217 1218 # generating info tables with all instruments by type: 1219 for iType in self.iList.keys(): 1220 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1221 1222 for instrument in self.iList[iType].keys(): 1223 iName = self.iList[iType][instrument]["name"] # instrument's name 1224 if len(iName) > 57: 1225 iName = "{}...".format(iName[:54]) # right trim for a long string 1226 1227 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1228 self.iList[iType][instrument]["ticker"], 1229 iName, 1230 self.iList[iType][instrument]["figi"], 1231 self.iList[iType][instrument]["currency"], 1232 self.iList[iType][instrument]["lot"], 1233 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1234 )) 1235 1236 infoText = "".join(info) 1237 1238 if show and not onlyFiles: 1239 uLogger.info(infoText) 1240 1241 if self.instrumentsFile and (show or onlyFiles): 1242 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1243 fH.write(infoText) 1244 1245 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1246 1247 if self.useHTMLReports: 1248 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1249 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1250 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1251 1252 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1253 1254 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multi-lines string with all available broker instruments.
1256 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1257 """ 1258 This method search and show information about instruments by part of its ticker, FIGI or name. 1259 If `searchResultsFile` string is not empty then also save information to this file. 1260 1261 :param pattern: string with part of ticker, FIGI or instrument's name. 1262 :param show: if `True` then print results to console, if `False` — return list of result only. 1263 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1264 :return: list of dictionaries with all found instruments. 1265 """ 1266 if not self.iList: 1267 self.iList = self.Listing() 1268 1269 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1270 compiledPattern = re.compile(pattern, re.IGNORECASE) 1271 1272 for iType in self.iList: 1273 for instrument in self.iList[iType].values(): 1274 searchResult = compiledPattern.search(" ".join( 1275 [instrument["ticker"], instrument["figi"], instrument["name"]] 1276 )) 1277 1278 if searchResult: 1279 searchResults[iType][instrument["ticker"]] = instrument 1280 1281 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1282 info = [ 1283 "# Search results\n\n", 1284 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1285 "* **Search pattern:** [{}]\n".format(pattern), 1286 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1287 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1288 ] 1289 infoShort = info[:] 1290 1291 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1292 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1293 skippedLine = "| ... | ... | ... | ... |\n" 1294 1295 if resultsLen == 0: 1296 info.append("\nNo results\n") 1297 infoShort.append("\nNo results\n") 1298 uLogger.warning("No results. Try changing your search pattern.") 1299 1300 else: 1301 for iType in searchResults: 1302 iTypeValuesCount = len(searchResults[iType].values()) 1303 if iTypeValuesCount > 0: 1304 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1305 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1306 1307 for instrument in searchResults[iType].values(): 1308 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1309 instrument["type"], 1310 instrument["ticker"], 1311 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1312 instrument["figi"], 1313 )) 1314 1315 if iTypeValuesCount <= 5: 1316 infoShort.extend(info[-iTypeValuesCount:]) 1317 1318 else: 1319 infoShort.extend(info[-5:]) 1320 infoShort.append(skippedLine) 1321 1322 infoText = "".join(info) 1323 infoTextShort = "".join(infoShort) 1324 1325 if show and not onlyFiles: 1326 uLogger.info(infoTextShort) 1327 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1328 1329 if self.searchResultsFile and (show or onlyFiles): 1330 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1331 fH.write(infoText) 1332 1333 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1334 1335 if self.useHTMLReports: 1336 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1337 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1338 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1339 1340 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1341 1342 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
list of dictionaries with all found instruments.
1344 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1345 """ 1346 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1347 1348 :param instruments: list of strings with tickers or FIGIs. 1349 :return: list with unique instrument FIGIs only. 1350 """ 1351 requestedInstruments = [] 1352 for iName in instruments: 1353 if iName not in self.aliases.keys(): 1354 if iName not in requestedInstruments: 1355 requestedInstruments.append(iName) 1356 1357 else: 1358 if iName not in requestedInstruments: 1359 if self.aliases[iName] not in requestedInstruments: 1360 requestedInstruments.append(self.aliases[iName]) 1361 1362 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1363 1364 onlyUniqueFIGIs = [] 1365 for iName in requestedInstruments: 1366 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1367 continue 1368 1369 self._ticker = iName 1370 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1371 1372 if not iData: 1373 self._ticker = "" 1374 self._figi = iName 1375 1376 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1377 1378 if not iData: 1379 self._figi = "" 1380 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1381 1382 if iData and iData["figi"] not in onlyUniqueFIGIs: 1383 onlyUniqueFIGIs.append(iData["figi"]) 1384 1385 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1386 1387 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1389 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1390 """ 1391 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1392 1393 See limits: https://tinkoff.github.io/investAPI/limits/ 1394 1395 If `pricesFile` string is not empty then also save information to this file. 1396 1397 :param instruments: list of strings with tickers or FIGIs. 1398 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1399 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1400 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1401 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1402 """ 1403 if instruments is None or not instruments: 1404 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1405 raise Exception("Ticker or FIGI required") 1406 1407 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1408 1409 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1410 1411 iList = [] # trying to get info and current prices about all unique instruments: 1412 for self._figi in onlyUniqueFIGIs: 1413 iData = self.SearchByFIGI(requestPrice=True, show=False) 1414 iList.append(iData) 1415 1416 self.ShowListOfPrices(iList, show, onlyFiles) 1417 1418 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1420 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1421 """ 1422 Show table contains current prices of given instruments. 1423 1424 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1425 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1426 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1427 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1428 :return: multilines text in Markdown format as a table contains current prices. 1429 """ 1430 infoText = "" 1431 1432 if show or self.pricesFile or onlyFiles: 1433 info = [ 1434 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1435 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1436 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1437 ] 1438 1439 for item in iList: 1440 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1441 item["ticker"], 1442 item["figi"], 1443 item["type"], 1444 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1445 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1446 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1447 "{} / {}".format( 1448 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1449 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1450 ), 1451 "{} / {}".format( 1452 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1453 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1454 ), 1455 item["currency"], 1456 )) 1457 1458 infoText = "".join(info) 1459 1460 if show and not onlyFiles: 1461 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1462 1463 if self.pricesFile and (show or onlyFiles): 1464 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1465 fH.write(infoText) 1466 1467 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1468 1469 if self.useHTMLReports: 1470 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1471 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1472 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1473 1474 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1475 1476 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format as a table contains current prices.
1478 def RequestTradingStatus(self) -> dict: 1479 """ 1480 Requesting trading status for the instrument defined by `figi` variable. 1481 1482 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1483 1484 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1485 1486 :return: dictionary with trading status attributes. Response example: 1487 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1488 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1489 """ 1490 if self._figi is None or not self._figi: 1491 uLogger.error("Variable `figi` must be defined for using this method!") 1492 raise Exception("FIGI required") 1493 1494 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1495 1496 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1497 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1498 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1499 1500 if self.moreDebug: 1501 uLogger.debug("Records about current trading status successfully received") 1502 1503 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1505 def RequestPortfolio(self) -> dict: 1506 """ 1507 Requesting actual user's portfolio for current `accountId`. 1508 1509 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1510 1511 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1512 1513 :return: dictionary with user's portfolio. 1514 """ 1515 if self.accountId is None or not self.accountId: 1516 uLogger.error("Variable `accountId` must be defined for using this method!") 1517 raise Exception("Account ID required") 1518 1519 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1520 1521 self.body = str({"accountId": self.accountId}) 1522 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1523 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1524 1525 if self.moreDebug: 1526 uLogger.debug("Records about user's portfolio successfully received") 1527 1528 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1530 def RequestPositions(self) -> dict: 1531 """ 1532 Requesting open positions by currencies and instruments for current `accountId`. 1533 1534 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1535 1536 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1537 1538 :return: dictionary with open positions by instruments. 1539 """ 1540 if self.accountId is None or not self.accountId: 1541 uLogger.error("Variable `accountId` must be defined for using this method!") 1542 raise Exception("Account ID required") 1543 1544 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1545 1546 self.body = str({"accountId": self.accountId}) 1547 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1548 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1549 1550 if self.moreDebug: 1551 uLogger.debug("Records about current open positions successfully received") 1552 1553 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1555 def RequestPendingOrders(self) -> list: 1556 """ 1557 Requesting current actual pending limit orders for current `accountId`. 1558 1559 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1560 1561 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1562 1563 :return: list of dictionaries with pending limit orders. 1564 """ 1565 if self.accountId is None or not self.accountId: 1566 uLogger.error("Variable `accountId` must be defined for using this method!") 1567 raise Exception("Account ID required") 1568 1569 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1570 1571 self.body = str({"accountId": self.accountId}) 1572 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1573 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1574 1575 if "orders" in rawResponse.keys(): 1576 rawOrders = rawResponse["orders"] 1577 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1578 1579 else: 1580 rawOrders = [] 1581 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1582 1583 return rawOrders
Requesting current actual pending limit orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1585 def RequestStopOrders(self) -> list: 1586 """ 1587 Requesting current actual stop orders for current `accountId`. 1588 1589 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1590 1591 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1592 1593 :return: list of dictionaries with stop orders. 1594 """ 1595 if self.accountId is None or not self.accountId: 1596 uLogger.error("Variable `accountId` must be defined for using this method!") 1597 raise Exception("Account ID required") 1598 1599 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1600 1601 self.body = str({"accountId": self.accountId}) 1602 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1603 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1604 1605 if "stopOrders" in rawResponse.keys(): 1606 rawStopOrders = rawResponse["stopOrders"] 1607 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1608 1609 else: 1610 rawStopOrders = [] 1611 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1612 1613 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1615 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1616 """ 1617 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1618 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1619 and `overviewBondsCalendarFile` are defined then also save information to file. 1620 1621 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1622 many requests about the state of the portfolio, and then, based on the received data, a large number 1623 of calculation and statistics are collected. 1624 1625 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1626 :param details: how detailed should the information be? 1627 - `full` — shows full available information about portfolio status (by default), 1628 - `positions` — shows only open positions, 1629 - `orders` — shows only sections of open limits and stop orders. 1630 - `digest` — show a short digest of the portfolio status, 1631 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1632 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1633 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1634 :return: dictionary with client's raw portfolio and some statistics. 1635 """ 1636 if self.accountId is None or not self.accountId: 1637 uLogger.error("Variable `accountId` must be defined for using this method!") 1638 raise Exception("Account ID required") 1639 1640 view = { 1641 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1642 "headers": {}, # list of dictionaries, response headers without "positions" section 1643 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1644 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1645 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1646 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1647 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1648 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1649 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1650 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1651 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1652 }, 1653 "stat": { # --- some statistics calculated using "raw" sections: 1654 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1655 "availableRUB": 0., # available rubles (without other currencies) 1656 "blockedRUB": 0., # blocked sum in Russian Rouble 1657 "totalChangesRUB": 0., # changes for all open trades in RUB 1658 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1659 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1660 "sharesCostRUB": 0., # costs of all shares in RUB 1661 "bondsCostRUB": 0., # costs of all bonds in RUB 1662 "etfsCostRUB": 0., # costs of all etfs in RUB 1663 "futuresCostRUB": 0., # costs of all futures in RUB 1664 "Currencies": [], # list of dictionaries of all currencies statistics 1665 "Shares": [], # list of dictionaries of all shares statistics 1666 "Bonds": [], # list of dictionaries of all bonds statistics 1667 "Etfs": [], # list of dictionaries of all etfs statistics 1668 "Futures": [], # list of dictionaries of all futures statistics 1669 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1670 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1671 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1672 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1673 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1674 }, 1675 "analytics": { # --- some analytics of portfolio: 1676 "distrByAssets": {}, # portfolio distribution by assets 1677 "distrByCompanies": {}, # portfolio distribution by companies 1678 "distrBySectors": {}, # portfolio distribution by sectors 1679 "distrByCurrencies": {}, # portfolio distribution by currencies 1680 "distrByCountries": {}, # portfolio distribution by countries 1681 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1682 } 1683 } 1684 1685 details = details.lower() 1686 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1687 if details not in availableDetails: 1688 details = "full" 1689 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1690 1691 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1692 1693 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1694 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1695 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1696 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1697 1698 # save response headers without "positions" section: 1699 for key in portfolioResponse.keys(): 1700 if key != "positions": 1701 view["raw"]["headers"][key] = portfolioResponse[key] 1702 1703 else: 1704 continue 1705 1706 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1707 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1708 for item in portfolioResponse["positions"]: 1709 if item["instrumentType"] == "currency": 1710 self._figi = item["figi"] 1711 if not self._figi and item["ticker"]: 1712 self._ticker = item["ticker"] 1713 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1714 1715 curr = self.SearchByFIGI(requestPrice=False) 1716 1717 # current price of currency in RUB: 1718 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1719 "name": curr["name"], 1720 "currentPrice": NanoToFloat( 1721 item["currentPrice"]["units"], 1722 item["currentPrice"]["nano"] 1723 ), 1724 } 1725 1726 view["raw"]["Currencies"].append(item) 1727 1728 elif item["instrumentType"] == "share": 1729 view["raw"]["Shares"].append(item) 1730 1731 elif item["instrumentType"] == "bond": 1732 view["raw"]["Bonds"].append(item) 1733 1734 elif item["instrumentType"] == "etf": 1735 view["raw"]["Etfs"].append(item) 1736 1737 elif item["instrumentType"] == "futures": 1738 view["raw"]["Futures"].append(item) 1739 1740 else: 1741 continue 1742 1743 # how many volume of currencies (by ISO currency name) are blocked: 1744 for item in view["raw"]["positions"]["blocked"]: 1745 blocked = NanoToFloat(item["units"], item["nano"]) 1746 if blocked > 0: 1747 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1748 1749 # how many volume of instruments (by FIGI) are blocked: 1750 for item in view["raw"]["positions"]["securities"]: 1751 blocked = int(item["blocked"]) 1752 if blocked > 0: 1753 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1754 1755 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1756 1757 if "rub" in allBlocked.keys(): 1758 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1759 1760 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1761 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1762 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1763 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1764 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1765 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1766 view["stat"]["portfolioCostRUB"] = sum([ 1767 view["stat"]["allCurrenciesCostRUB"], 1768 view["stat"]["sharesCostRUB"], 1769 view["stat"]["bondsCostRUB"], 1770 view["stat"]["etfsCostRUB"], 1771 view["stat"]["futuresCostRUB"], 1772 ]) 1773 1774 # --- calculating some portfolio statistics: 1775 byComp = {} # distribution by companies 1776 bySect = {} # distribution by sectors 1777 byCurr = {} # distribution by currencies (include RUB) 1778 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1779 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1780 1781 for item in portfolioResponse["positions"]: 1782 self._figi = item["figi"] 1783 if not self._figi and item["ticker"]: 1784 self._ticker = item["ticker"] 1785 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1786 1787 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1788 1789 if instrument: 1790 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1791 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1792 1793 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1794 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1795 1796 else: 1797 blocked = 0 1798 1799 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1800 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1801 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1802 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1803 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1804 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1805 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1806 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1807 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1808 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1809 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1810 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1811 1812 statData = { 1813 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1814 "ticker": instrument["ticker"], # ticker by FIGI 1815 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1816 "volume": volume, # available volume of instrument 1817 "lots": lots, # volume in lots of instrument 1818 "direction": direction, # direction of an instrument's position: short or long 1819 "blocked": blocked, # blocked volume of currency or instrument 1820 "currentPrice": curPrice, # current instrument's price in basic asset 1821 "average": average, # current average position price 1822 "cost": cost, # current cost of all volume of instrument in basic asset 1823 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1824 "costRUB": costRUB, # cost of instrument in ruble 1825 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1826 "profit": profit, # expected profit at current moment 1827 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1828 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1829 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1830 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1831 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1832 "step": instrument["step"], # minimum price increment 1833 } 1834 1835 # adding distribution by unique countries: 1836 if statData["country"] not in byCountry.keys(): 1837 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1838 1839 else: 1840 byCountry[statData["country"]]["cost"] += costRUB 1841 byCountry[statData["country"]]["percent"] += percentCostRUB 1842 1843 if item["instrumentType"] != "currency": 1844 # adding distribution by unique companies: 1845 if statData["name"]: 1846 if statData["name"] not in byComp.keys(): 1847 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1848 1849 else: 1850 byComp[statData["name"]]["cost"] += costRUB 1851 byComp[statData["name"]]["percent"] += percentCostRUB 1852 1853 # adding distribution by unique sectors: 1854 if statData["sector"] not in bySect.keys(): 1855 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1856 1857 else: 1858 bySect[statData["sector"]]["cost"] += costRUB 1859 bySect[statData["sector"]]["percent"] += percentCostRUB 1860 1861 # adding distribution by unique currencies: 1862 if currency not in byCurr.keys(): 1863 byCurr[currency] = { 1864 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1865 "cost": costRUB, 1866 "percent": percentCostRUB 1867 } 1868 1869 else: 1870 byCurr[currency]["cost"] += costRUB 1871 byCurr[currency]["percent"] += percentCostRUB 1872 1873 # saving statistics for every instrument: 1874 if item["instrumentType"] == "currency": 1875 view["stat"]["Currencies"].append(statData) 1876 1877 # update dict with free funds for trading (total - blocked) by currencies 1878 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1879 view["stat"]["funds"][currency] = { 1880 "total": volume, 1881 "totalCostRUB": costRUB, # total volume cost in rubles 1882 "free": volume - blocked, 1883 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1884 } 1885 1886 elif item["instrumentType"] == "share": 1887 view["stat"]["Shares"].append(statData) 1888 1889 elif item["instrumentType"] == "bond": 1890 view["stat"]["Bonds"].append(statData) 1891 1892 elif item["instrumentType"] == "etf": 1893 view["stat"]["Etfs"].append(statData) 1894 1895 elif item["instrumentType"] == "Futures": 1896 view["stat"]["Futures"].append(statData) 1897 1898 else: 1899 continue 1900 1901 # total changes in Russian Ruble: 1902 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1903 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1904 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1905 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1906 view["stat"]["funds"]["rub"] = { 1907 "total": view["stat"]["availableRUB"], 1908 "totalCostRUB": view["stat"]["availableRUB"], 1909 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1910 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1911 } 1912 1913 # --- pending limit orders sector data: 1914 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1915 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1916 1917 for item in view["raw"]["orders"]: 1918 self._figi = item["figi"] 1919 1920 if item["figi"] not in uniquePendingOrdersFIGIs: 1921 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1922 1923 uniquePendingOrdersFIGIs.append(item["figi"]) 1924 uniquePendingOrders[item["figi"]] = instrument 1925 1926 else: 1927 instrument = uniquePendingOrders[item["figi"]] 1928 1929 if instrument: 1930 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1931 orderType = TKS_ORDER_TYPES[item["orderType"]] 1932 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1933 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1934 1935 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1936 if item["direction"] == "ORDER_DIRECTION_BUY": 1937 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1938 1939 else: 1940 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1941 1942 # requested price for order execution: 1943 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1944 1945 # necessary changes in percent to reach target from current price: 1946 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1947 1948 view["stat"]["orders"].append({ 1949 "orderID": item["orderId"], # orderId number parameter of current order 1950 "figi": item["figi"], # FIGI identification 1951 "ticker": instrument["ticker"], # ticker name by FIGI 1952 "lotsRequested": item["lotsRequested"], # requested lots value 1953 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1954 "currentPrice": lastPrice, # current instrument's price for defined action 1955 "targetPrice": target, # requested price for order execution in base currency 1956 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1957 "percentChanges": changes, # changes in percent to target from current price 1958 "currency": item["currency"], # instrument's currency name 1959 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1960 "type": orderType, # type of order from TKS_ORDER_TYPES 1961 "status": orderState, # order status from TKS_ORDER_STATES 1962 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1963 }) 1964 1965 # --- stop orders sector data: 1966 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1967 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1968 1969 for item in view["raw"]["stopOrders"]: 1970 self._figi = item["figi"] 1971 1972 if item["figi"] not in uniqueStopOrdersFIGIs: 1973 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1974 1975 uniqueStopOrdersFIGIs.append(item["figi"]) 1976 uniqueStopOrders[item["figi"]] = instrument 1977 1978 else: 1979 instrument = uniqueStopOrders[item["figi"]] 1980 1981 if instrument: 1982 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1983 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1984 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1985 1986 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1987 if "expirationTime" in item.keys(): 1988 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1989 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1990 1991 else: 1992 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1993 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1994 1995 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1996 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1997 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1998 1999 else: 2000 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2001 2002 # requested price when stop-order executed: 2003 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2004 2005 # price for limit-order, set up when stop-order executed: 2006 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2007 2008 # necessary changes in percent to reach target from current price: 2009 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2010 2011 view["stat"]["stopOrders"].append({ 2012 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2013 "figi": item["figi"], # FIGI identification 2014 "ticker": instrument["ticker"], # ticker name by FIGI 2015 "lotsRequested": item["lotsRequested"], # requested lots value 2016 "currentPrice": lastPrice, # current instrument's price for defined action 2017 "targetPrice": target, # requested price for stop-order execution in base currency 2018 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2019 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2020 "percentChanges": changes, # changes in percent to target from current price 2021 "currency": item["currency"], # instrument's currency name 2022 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2023 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2024 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2025 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2026 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2027 }) 2028 2029 # --- calculating data for analytics section: 2030 # portfolio distribution by assets: 2031 view["analytics"]["distrByAssets"] = { 2032 "Ruble": { 2033 "uniques": 1, 2034 "cost": view["stat"]["availableRUB"], 2035 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2036 }, 2037 "Currencies": { 2038 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2039 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2040 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2041 }, 2042 "Shares": { 2043 "uniques": len(view["stat"]["Shares"]), 2044 "cost": view["stat"]["sharesCostRUB"], 2045 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2046 }, 2047 "Bonds": { 2048 "uniques": len(view["stat"]["Bonds"]), 2049 "cost": view["stat"]["bondsCostRUB"], 2050 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2051 }, 2052 "Etfs": { 2053 "uniques": len(view["stat"]["Etfs"]), 2054 "cost": view["stat"]["etfsCostRUB"], 2055 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2056 }, 2057 "Futures": { 2058 "uniques": len(view["stat"]["Futures"]), 2059 "cost": view["stat"]["futuresCostRUB"], 2060 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2061 }, 2062 } 2063 2064 # portfolio distribution by companies: 2065 view["analytics"]["distrByCompanies"]["All money cash"] = { 2066 "ticker": "", 2067 "cost": view["stat"]["allCurrenciesCostRUB"], 2068 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2069 } 2070 view["analytics"]["distrByCompanies"].update(byComp) 2071 2072 # portfolio distribution by sectors: 2073 view["analytics"]["distrBySectors"]["All money cash"] = { 2074 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2075 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2076 } 2077 view["analytics"]["distrBySectors"].update(bySect) 2078 2079 # portfolio distribution by currencies: 2080 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2081 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2082 2083 if self.moreDebug: 2084 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2085 2086 view["analytics"]["distrByCurrencies"].update(byCurr) 2087 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2088 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2089 2090 # portfolio distribution by countries: 2091 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2092 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2093 2094 if self.moreDebug: 2095 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2096 2097 view["analytics"]["distrByCountries"].update(byCountry) 2098 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2099 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2100 2101 # --- Prepare text statistics overview in human-readable: 2102 if show or onlyFiles: 2103 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2104 2105 # Whatever the value `details`, header not changes: 2106 info = [ 2107 "# Client's portfolio\n\n", 2108 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2109 "* **Account ID:** [{}]\n".format(self.accountId), 2110 ] 2111 2112 if details in ["full", "positions", "digest"]: 2113 info.extend([ 2114 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2115 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2116 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2117 view["stat"]["totalChangesRUB"], 2118 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2119 view["stat"]["totalChangesPercentRUB"], 2120 ), 2121 ]) 2122 2123 if details in ["full", "positions"]: 2124 info.extend([ 2125 "## Open positions\n\n", 2126 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2127 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2128 "| **Ruble:** | {:>31} | | | | | |\n".format( 2129 "{:.2f} ({:.2f}) rub".format( 2130 view["stat"]["availableRUB"], 2131 view["stat"]["blockedRUB"], 2132 ) 2133 ) 2134 ]) 2135 2136 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2137 return [ 2138 "| | | | | | | |\n", 2139 "| {:<27} | | | | | {:>19} | |\n".format( 2140 noTradeStr if noTradeStr else typeStr, 2141 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2142 ), 2143 ] 2144 2145 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2146 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2147 "{} [{}]".format(data["ticker"], data["figi"]), 2148 "{:.2f} ({:.2f}) {}".format( 2149 data["volume"], 2150 data["blocked"], 2151 data["currency"], 2152 ) if isCurr else "{:.0f} ({:.0f})".format( 2153 data["volume"], 2154 data["blocked"], 2155 ), 2156 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2157 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2158 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2159 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2160 "{}{:.2f} {} ({}{:.2f}%)".format( 2161 "+" if data["profit"] > 0 else "", 2162 data["profit"], data["baseCurrencyName"], 2163 "+" if data["percentProfit"] > 0 else "", 2164 data["percentProfit"], 2165 ), 2166 ) 2167 2168 # --- Show currencies section: 2169 if view["stat"]["Currencies"]: 2170 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2171 for item in view["stat"]["Currencies"]: 2172 info.append(_InfoStr(item, isCurr=True)) 2173 2174 else: 2175 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2176 2177 # --- Show shares section: 2178 if view["stat"]["Shares"]: 2179 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2180 2181 for item in view["stat"]["Shares"]: 2182 info.append(_InfoStr(item)) 2183 2184 else: 2185 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2186 2187 # --- Show bonds section: 2188 if view["stat"]["Bonds"]: 2189 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2190 2191 for item in view["stat"]["Bonds"]: 2192 info.append(_InfoStr(item)) 2193 2194 else: 2195 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2196 2197 # --- Show etfs section: 2198 if view["stat"]["Etfs"]: 2199 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2200 2201 for item in view["stat"]["Etfs"]: 2202 info.append(_InfoStr(item)) 2203 2204 else: 2205 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2206 2207 # --- Show futures section: 2208 if view["stat"]["Futures"]: 2209 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2210 2211 for item in view["stat"]["Futures"]: 2212 info.append(_InfoStr(item)) 2213 2214 else: 2215 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2216 2217 if details in ["full", "orders"]: 2218 # --- Show pending limit orders section: 2219 if view["stat"]["orders"]: 2220 info.extend([ 2221 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2222 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2223 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2224 ]) 2225 2226 for item in view["stat"]["orders"]: 2227 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2228 "{} [{}]".format(item["ticker"], item["figi"]), 2229 item["orderID"], 2230 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2231 "{} {} ({}{:.2f}%)".format( 2232 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2233 item["baseCurrencyName"], 2234 "+" if item["percentChanges"] > 0 else "", 2235 float(item["percentChanges"]), 2236 ), 2237 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2238 item["action"], 2239 item["type"], 2240 item["date"], 2241 )) 2242 2243 else: 2244 info.append("\n## Total pending limit-orders: [0]\n") 2245 2246 # --- Show stop orders section: 2247 if view["stat"]["stopOrders"]: 2248 info.extend([ 2249 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2250 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2251 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2252 ]) 2253 2254 for item in view["stat"]["stopOrders"]: 2255 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2256 "{} [{}]".format(item["ticker"], item["figi"]), 2257 item["orderID"], 2258 item["lotsRequested"], 2259 "{} {} ({}{:.2f}%)".format( 2260 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2261 item["baseCurrencyName"], 2262 "+" if item["percentChanges"] > 0 else "", 2263 float(item["percentChanges"]), 2264 ), 2265 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2266 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2267 item["action"], 2268 item["type"], 2269 item["expType"], 2270 item["createDate"], 2271 item["expDate"], 2272 )) 2273 2274 else: 2275 info.append("\n## Total stop-orders: [0]\n") 2276 2277 if details in ["full", "analytics"]: 2278 # -- Show analytics section: 2279 if view["stat"]["portfolioCostRUB"] > 0: 2280 info.extend([ 2281 "\n# Analytics\n\n" 2282 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2283 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2284 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2285 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2286 view["stat"]["totalChangesRUB"], 2287 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2288 view["stat"]["totalChangesPercentRUB"], 2289 ), 2290 "\n## Portfolio distribution by assets\n" 2291 "\n| Type | Uniques | Percent | Current cost |\n", 2292 "|------------------------------------|---------|---------|--------------------|\n", 2293 ]) 2294 2295 for key in view["analytics"]["distrByAssets"].keys(): 2296 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2297 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2298 key, 2299 view["analytics"]["distrByAssets"][key]["uniques"], 2300 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2301 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2302 )) 2303 2304 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2305 2306 info.extend([ 2307 "\n## Portfolio distribution by companies\n" 2308 "\n| Company | Percent | Current cost |\n", 2309 aSepLine, 2310 ]) 2311 2312 for company in view["analytics"]["distrByCompanies"].keys(): 2313 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2314 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2315 "{}{}".format( 2316 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2317 company, 2318 ), 2319 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2320 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2321 )) 2322 2323 info.extend([ 2324 "\n## Portfolio distribution by sectors\n" 2325 "\n| Sector | Percent | Current cost |\n", 2326 aSepLine, 2327 ]) 2328 2329 for sector in view["analytics"]["distrBySectors"].keys(): 2330 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2331 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2332 sector, 2333 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2334 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2335 )) 2336 2337 info.extend([ 2338 "\n## Portfolio distribution by currencies\n" 2339 "\n| Instruments currencies | Percent | Current cost |\n", 2340 aSepLine, 2341 ]) 2342 2343 for curr in view["analytics"]["distrByCurrencies"].keys(): 2344 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2345 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2346 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2347 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2348 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2349 )) 2350 2351 info.extend([ 2352 "\n## Portfolio distribution by countries\n" 2353 "\n| Assets by country | Percent | Current cost |\n", 2354 aSepLine, 2355 ]) 2356 2357 for country in view["analytics"]["distrByCountries"].keys(): 2358 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2359 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2360 country, 2361 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2362 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2363 )) 2364 2365 if details in ["full", "calendar"]: 2366 # -- Show bonds payment calendar section: 2367 if view["stat"]["Bonds"]: 2368 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2369 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2370 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2371 2372 else: 2373 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2374 2375 infoText = "".join(info) 2376 2377 if show and not onlyFiles: 2378 uLogger.info(infoText) 2379 2380 if details == "full" and self.overviewFile: 2381 filename = self.overviewFile 2382 2383 elif details == "digest" and self.overviewDigestFile: 2384 filename = self.overviewDigestFile 2385 2386 elif details == "positions" and self.overviewPositionsFile: 2387 filename = self.overviewPositionsFile 2388 2389 elif details == "orders" and self.overviewOrdersFile: 2390 filename = self.overviewOrdersFile 2391 2392 elif details == "analytics" and self.overviewAnalyticsFile: 2393 filename = self.overviewAnalyticsFile 2394 2395 elif details == "calendar" and self.overviewBondsCalendarFile: 2396 filename = self.overviewBondsCalendarFile 2397 2398 else: 2399 filename = "" 2400 2401 if filename and (show or onlyFiles): 2402 with open(filename, "w", encoding="UTF-8") as fH: 2403 fH.write(infoText) 2404 2405 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2406 2407 if self.useHTMLReports: 2408 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2409 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2410 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2411 2412 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2413 2414 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio).
- onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dictionary with client's raw portfolio and some statistics.
2416 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2417 """ 2418 Returns history operations between two given dates for current `accountId`. 2419 If `reportFile` string is not empty then also save human-readable report. 2420 Shows some statistical data of closed positions. 2421 2422 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2423 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2424 :param show: if `True` then also prints all records to the console. 2425 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2426 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2427 :return: original list of dictionaries with history of deals records from API ("operations" key): 2428 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2429 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2430 """ 2431 if self.accountId is None or not self.accountId: 2432 uLogger.error("Variable `accountId` must be defined for using this method!") 2433 raise Exception("Account ID required") 2434 2435 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2436 2437 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2438 2439 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2440 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2441 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2442 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2443 customStat = {} # custom statistics in additional to responseJSON 2444 2445 # --- output report in human-readable format: 2446 if show or onlyFiles or self.reportFile: 2447 splitLine1 = "| | | | | |\n" # Summary section 2448 splitLine2 = "| | | | | | | | |\n" # Operations section 2449 nextDay = "" 2450 2451 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2452 2453 if len(ops) > 0: 2454 customStat = { 2455 "opsCount": 0, # total operations count 2456 "buyCount": 0, # buy operations 2457 "sellCount": 0, # sell operations 2458 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2459 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2460 "payIn": {"rub": 0.}, # Deposit brokerage account 2461 "payOut": {"rub": 0.}, # Withdrawals 2462 "divs": {"rub": 0.}, # Dividends income 2463 "coupons": {"rub": 0.}, # Coupon's income 2464 "brokerCom": {"rub": 0.}, # Service commissions 2465 "serviceCom": {"rub": 0.}, # Service commissions 2466 "marginCom": {"rub": 0.}, # Margin commissions 2467 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2468 } 2469 2470 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2471 for item in ops: 2472 if item["state"] == "OPERATION_STATE_EXECUTED": 2473 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2474 2475 # count buy operations: 2476 if "_BUY" in item["operationType"]: 2477 customStat["buyCount"] += 1 2478 2479 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2480 customStat["buyTotal"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["buyTotal"][item["payment"]["currency"]] = payment 2484 2485 # count sell operations: 2486 elif "_SELL" in item["operationType"]: 2487 customStat["sellCount"] += 1 2488 2489 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2490 customStat["sellTotal"][item["payment"]["currency"]] += payment 2491 2492 else: 2493 customStat["sellTotal"][item["payment"]["currency"]] = payment 2494 2495 # count incoming operations: 2496 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2497 if item["payment"]["currency"] in customStat["payIn"].keys(): 2498 customStat["payIn"][item["payment"]["currency"]] += payment 2499 2500 else: 2501 customStat["payIn"][item["payment"]["currency"]] = payment 2502 2503 # count withdrawals operations: 2504 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2505 if item["payment"]["currency"] in customStat["payOut"].keys(): 2506 customStat["payOut"][item["payment"]["currency"]] += payment 2507 2508 else: 2509 customStat["payOut"][item["payment"]["currency"]] = payment 2510 2511 # count dividends income: 2512 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2513 if item["payment"]["currency"] in customStat["divs"].keys(): 2514 customStat["divs"][item["payment"]["currency"]] += payment 2515 2516 else: 2517 customStat["divs"][item["payment"]["currency"]] = payment 2518 2519 # count coupon's income: 2520 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2521 if item["payment"]["currency"] in customStat["coupons"].keys(): 2522 customStat["coupons"][item["payment"]["currency"]] += payment 2523 2524 else: 2525 customStat["coupons"][item["payment"]["currency"]] = payment 2526 2527 # count broker commissions: 2528 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2529 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2530 customStat["brokerCom"][item["payment"]["currency"]] += payment 2531 2532 else: 2533 customStat["brokerCom"][item["payment"]["currency"]] = payment 2534 2535 # count service commissions: 2536 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2537 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2538 customStat["serviceCom"][item["payment"]["currency"]] += payment 2539 2540 else: 2541 customStat["serviceCom"][item["payment"]["currency"]] = payment 2542 2543 # count margin commissions: 2544 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2545 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2546 customStat["marginCom"][item["payment"]["currency"]] += payment 2547 2548 else: 2549 customStat["marginCom"][item["payment"]["currency"]] = payment 2550 2551 # count withholding taxes: 2552 elif "_TAX" in item["operationType"]: 2553 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2554 customStat["allTaxes"][item["payment"]["currency"]] += payment 2555 2556 else: 2557 customStat["allTaxes"][item["payment"]["currency"]] = payment 2558 2559 else: 2560 continue 2561 2562 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2563 2564 # --- view "Actions" lines: 2565 info.extend([ 2566 "| Report sections | | | | |\n", 2567 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2568 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2569 "| | Buy: {:<22} | {:<28} | | |\n".format( 2570 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2571 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2572 ), 2573 "| | Sell: {:<21} | {:<28} | | |\n".format( 2574 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2575 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2576 ), 2577 ]) 2578 2579 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2580 for key in opsKeys: 2581 if key == "rub": 2582 continue 2583 2584 info.extend([ 2585 "| | | {:<28} | | |\n".format( 2586 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2587 ), 2588 "| | | {:<28} | | |\n".format( 2589 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2590 ), 2591 ]) 2592 2593 info.append(splitLine1) 2594 2595 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2596 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2597 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2598 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2599 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2600 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2601 ) 2602 2603 # --- view "Payments" lines: 2604 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2605 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2606 2607 for key in paymentsKeys: 2608 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2609 2610 info.append(splitLine1) 2611 2612 # --- view "Commissions and taxes" lines: 2613 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2614 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2615 2616 for key in comKeys: 2617 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2618 2619 info.extend([ 2620 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2621 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2622 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2623 ]) 2624 2625 else: 2626 info.append("Broker returned no operations during this period\n") 2627 2628 # --- view "Operations" section: 2629 for item in ops: 2630 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2631 continue 2632 2633 else: 2634 self._figi = item["figi"] 2635 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2636 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2637 2638 # group of deals during one day: 2639 if nextDay and item["date"].split("T")[0] != nextDay: 2640 info.append(splitLine2) 2641 nextDay = "" 2642 2643 else: 2644 nextDay = item["date"].split("T")[0] # saving current day for splitting 2645 2646 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2647 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2648 self._figi if self._figi else "—", 2649 instrument["ticker"] if instrument else "—", 2650 instrument["type"] if instrument else "—", 2651 item["quantity"] if int(item["quantity"]) > 0 else "—", 2652 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2653 TKS_OPERATION_STATES[item["state"]], 2654 TKS_OPERATION_TYPES[item["operationType"]], 2655 )) 2656 2657 infoText = "".join(info) 2658 2659 if show and not onlyFiles: 2660 if self.moreDebug: 2661 uLogger.debug("Records about history of a client's operations successfully received") 2662 2663 uLogger.info(infoText) 2664 2665 if self.reportFile and (show or onlyFiles): 2666 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2667 fH.write(infoText) 2668 2669 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2670 2671 if self.useHTMLReports: 2672 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2673 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2674 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2675 2676 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2677 2678 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2680 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2681 """ 2682 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2683 2684 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2685 Warning! Broker server used ISO UTC time by default. 2686 2687 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2688 Also, `historyFile` used to update history with `onlyMissing` parameter. 2689 2690 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2691 2692 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2693 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2694 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2695 `"hour"`, `"day"`. Default: `"hour"`. 2696 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2697 False by default. Warning! History appends only from last candle to current time 2698 with always update last candle! 2699 :param csvSep: separator if csv-file is used, `,` by default. 2700 :param show: if `True` then also prints Pandas DataFrame to the console. 2701 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2702 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2703 `["date", "time", "open", "high", "low", "close", "volume"]`. 2704 """ 2705 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2706 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2707 history = None # empty pandas object for history 2708 2709 if interval not in TKS_CANDLE_INTERVALS.keys(): 2710 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2711 raise Exception("Incorrect value") 2712 2713 if not (self._ticker or self._figi): 2714 uLogger.error("Ticker or FIGI must be defined!") 2715 raise Exception("Ticker or FIGI required") 2716 2717 if self._ticker and not self._figi: 2718 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2719 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2720 2721 if self._figi and not self._ticker: 2722 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2723 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2724 2725 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2726 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2727 if interval.lower() != "day": 2728 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2729 2730 delta = dtEnd - dtStart # current UTC time minus last time in file 2731 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2732 2733 # calculate history length in candles: 2734 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2735 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2736 length += 1 # to avoid fraction time 2737 2738 # calculate data blocks count: 2739 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2740 2741 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2742 if self.moreDebug: 2743 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2744 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2745 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2746 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2747 2748 tempOld = None # pandas object for old history, if --only-missing key present 2749 lastTime = None # datetime object of last old candle in file 2750 2751 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2752 if self.moreDebug: 2753 uLogger.debug("--only-missing key present, add only last missing candles...") 2754 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2755 2756 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2757 2758 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2759 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2760 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2761 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2762 2763 # get last datetime object from last string in file or minus 1 delta if file is empty: 2764 if len(tempOld) > 0: 2765 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2766 2767 else: 2768 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2769 2770 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2771 2772 responseJSONs = [] # raw history blocks of data 2773 2774 blockEnd = dtEnd 2775 for item in range(blocks): 2776 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2777 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2778 2779 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2780 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2781 )) 2782 2783 if blockStart == blockEnd: 2784 uLogger.debug("Skipped this zero-length block...") 2785 2786 else: 2787 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2788 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2789 self.body = str({ 2790 "figi": self._figi, 2791 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2792 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2793 "interval": TKS_CANDLE_INTERVALS[interval][0] 2794 }) 2795 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2796 2797 if "code" in responseJSON.keys(): 2798 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2799 2800 else: 2801 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2802 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2803 2804 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2805 2806 blockEnd = blockStart 2807 2808 printCount = len(responseJSONs) # candles to show in console 2809 if responseJSONs: 2810 tempHistory = pd.DataFrame( 2811 data={ 2812 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2813 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2814 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2815 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2816 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2817 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2818 "volume": [int(item["volume"]) for item in responseJSONs], 2819 }, 2820 index=range(len(responseJSONs)), 2821 columns=["date", "time", "open", "high", "low", "close", "volume"], 2822 ) 2823 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2824 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2825 2826 # append only newest candles to old history if --only-missing key present: 2827 if onlyMissing and tempOld is not None and lastTime is not None: 2828 index = 0 # find start index in tempHistory data: 2829 2830 for i, item in tempHistory.iterrows(): 2831 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2832 2833 if curTime == lastTime: 2834 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2835 index = i 2836 printCount = index + 1 2837 break 2838 2839 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2840 2841 else: 2842 history = tempHistory # if no `--only-missing` key then load full data from server 2843 2844 if self.moreDebug: 2845 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2846 2847 if history is not None and not history.empty: 2848 if show and not onlyFiles: 2849 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2850 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2851 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2852 )) 2853 2854 else: 2855 uLogger.warning("Received an empty candles history!") 2856 2857 if self.historyFile is not None: 2858 if history is not None and not history.empty: 2859 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2860 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2861 2862 else: 2863 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2864 2865 else: 2866 if self.moreDebug: 2867 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2868 2869 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2871 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2872 """ 2873 Load candles history from csv-file and return Pandas DataFrame object. 2874 2875 See also: `History()` and `ShowHistoryChart()` methods. 2876 2877 :param filePath: path to csv-file to open. 2878 """ 2879 loadedHistory = None # init candles data object 2880 2881 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2882 2883 if os.path.exists(filePath): 2884 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2885 2886 tfStr = self.priceModel.FormattedDelta( 2887 self.priceModel.timeframe, 2888 "{days} days {hours}h {minutes}m {seconds}s", 2889 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2890 self.priceModel.timeframe, 2891 "{hours}h {minutes}m {seconds}s", 2892 ) 2893 2894 if loadedHistory is not None and not loadedHistory.empty: 2895 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2896 len(loadedHistory), 2897 tfStr, 2898 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2899 ) 2900 2901 else: 2902 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2903 2904 else: 2905 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2906 2907 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2909 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2910 """ 2911 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2912 2913 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2914 Default: `index.html` (both for interact and non-interact candlesticks chart). 2915 2916 See also: `History()` and `LoadHistory()` methods. 2917 2918 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2919 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2920 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2921 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2922 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2923 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2924 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2925 """ 2926 if isinstance(candles, str): 2927 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2928 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2929 2930 elif isinstance(candles, pd.DataFrame): 2931 self.priceModel.prices = candles # set candles chain from variable 2932 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2933 2934 if "datetime" not in candles.columns: 2935 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2936 2937 else: 2938 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2939 raise Exception("Incorrect value") 2940 2941 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2942 2943 if interact: 2944 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2945 2946 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2947 2948 else: 2949 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2950 2951 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2952 2953 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2955 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2956 """ 2957 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2958 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2959 2960 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2961 2962 :param operation: string "Buy" or "Sell". 2963 :param lots: volume, integer count of lots >= 1. 2964 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2965 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2966 :param expDate: string "Undefined" by default or local date in future, 2967 it is a string with format `%Y-%m-%d %H:%M:%S`. 2968 :return: JSON with response from broker server. 2969 """ 2970 if self.accountId is None or not self.accountId: 2971 uLogger.error("Variable `accountId` must be defined for using this method!") 2972 raise Exception("Account ID required") 2973 2974 if operation is None or not operation or operation not in ("Buy", "Sell"): 2975 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2976 raise Exception("Incorrect value") 2977 2978 if lots is None or lots < 1: 2979 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2980 lots = 1 2981 2982 if tp is None or tp < 0: 2983 tp = 0 2984 2985 if sl is None or sl < 0: 2986 sl = 0 2987 2988 if expDate is None or not expDate: 2989 expDate = "Undefined" 2990 2991 if not (self._ticker or self._figi): 2992 uLogger.error("Ticker or FIGI must be defined!") 2993 raise Exception("Ticker or FIGI required") 2994 2995 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2996 self._ticker = instrument["ticker"] 2997 self._figi = instrument["figi"] 2998 2999 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3000 3001 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3002 self.body = str({ 3003 "figi": self._figi, 3004 "quantity": str(lots), 3005 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3006 "accountId": str(self.accountId), 3007 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3008 }) 3009 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3010 3011 if "orderId" in response.keys(): 3012 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3013 operation, response["orderId"], 3014 self._ticker, self._figi, lots, 3015 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3016 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3017 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3018 )) 3019 3020 if tp > 0: 3021 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3022 3023 if sl > 0: 3024 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3025 3026 else: 3027 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3028 3029 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3031 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3032 """ 3033 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3034 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3035 3036 See also: `Order()` and `Trade()` docstrings. 3037 3038 :param lots: volume, integer count of lots >= 1. 3039 :param tp: float > 0, take profit price of stop-order. 3040 :param sl: float > 0, stop loss price of stop-order. 3041 :param expDate: it's a local date in future. 3042 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3043 :return: JSON with response from broker server. 3044 """ 3045 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3047 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3048 """ 3049 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3050 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3051 3052 See also: `Order()` and `Trade()` docstrings. 3053 3054 :param lots: volume, integer count of lots >= 1. 3055 :param tp: float > 0, take profit price of stop-order. 3056 :param sl: float > 0, stop loss price of stop-order. 3057 :param expDate: it's a local date in the future. 3058 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3059 :return: JSON with response from broker server. 3060 """ 3061 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3063 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3064 """ 3065 Close position of given instruments. 3066 3067 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3068 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3069 This avoids unnecessary downloading data from the server. 3070 """ 3071 if instruments is None or not instruments: 3072 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3073 raise Exception("Ticker or FIGI required") 3074 3075 if isinstance(instruments, str): 3076 instruments = [instruments] 3077 3078 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3079 if uniqueInstruments: 3080 if portfolio is None or not portfolio: 3081 portfolio = self.Overview(show=False) 3082 3083 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3084 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3085 3086 for self._figi in uniqueInstruments: 3087 if self._figi not in allOpened: 3088 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3089 continue 3090 3091 # search open trade info about instrument by ticker: 3092 instrument = {} 3093 for iType in TKS_INSTRUMENTS: 3094 if instrument: 3095 break 3096 3097 for item in portfolio["stat"][iType]: 3098 if item["figi"] == self._figi: 3099 instrument = item 3100 break 3101 3102 if instrument: 3103 self._ticker = instrument["ticker"] 3104 self._figi = instrument["figi"] 3105 3106 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3107 self._ticker, 3108 self._figi, 3109 int(instrument["volume"]), 3110 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3111 )) 3112 3113 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3114 3115 if tradeLots > 0: 3116 if instrument["blocked"] > 0: 3117 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3118 instrument["blocked"], 3119 self._ticker, 3120 tradeLots, 3121 )) 3122 3123 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3124 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3125 3126 else: 3127 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3129 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3130 """ 3131 Close all positions of given instruments with defined type. 3132 3133 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3134 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3135 This avoids unnecessary downloading data from the server. 3136 """ 3137 if iType not in TKS_INSTRUMENTS: 3138 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3139 3140 else: 3141 if portfolio is None or not portfolio: 3142 portfolio = self.Overview(show=False) 3143 3144 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3145 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3146 3147 if tickers and portfolio: 3148 self.CloseTrades(tickers, portfolio) 3149 3150 else: 3151 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3153 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3154 """ 3155 Universal method to create market or limit orders with all available parameters for current `accountId`. 3156 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3157 3158 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3159 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3160 3161 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3162 then broker immediately open market order as you can do simple --buy or --sell operations! 3163 3164 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3165 When current price will go up or down to target price value then broker opens a limit order. 3166 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3167 3168 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3169 3170 :param operation: string "Buy" or "Sell". 3171 :param orderType: string "Limit" or "Stop". 3172 :param lots: volume, integer count of lots >= 1. 3173 :param targetPrice: target price > 0. This is open trade price for limit order. 3174 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3175 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3176 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3177 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3178 Stop loss order always executed by market price. 3179 :param expDate: string "Undefined" by default or local date in future. 3180 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3181 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3182 A limit order has no expiration date, it lasts until the end of the trading day. 3183 :return: JSON with response from broker server. 3184 """ 3185 if self.accountId is None or not self.accountId: 3186 uLogger.error("Variable `accountId` must be defined for using this method!") 3187 raise Exception("Account ID required") 3188 3189 if operation is None or not operation or operation not in ("Buy", "Sell"): 3190 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3191 raise Exception("Incorrect value") 3192 3193 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3194 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3195 raise Exception("Incorrect value") 3196 3197 if lots is None or lots < 1: 3198 uLogger.error("You must define trade volume > 0: integer count of lots!") 3199 raise Exception("Incorrect value") 3200 3201 if targetPrice is None or targetPrice <= 0: 3202 uLogger.error("Target price for limit-order must be greater than 0!") 3203 raise Exception("Incorrect value") 3204 3205 if limitPrice is None or limitPrice <= 0: 3206 limitPrice = targetPrice 3207 3208 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3209 stopType = "Limit" 3210 3211 if expDate is None or not expDate: 3212 expDate = "Undefined" 3213 3214 if not (self._ticker or self._figi): 3215 uLogger.error("Tocker or FIGI must be defined!") 3216 raise Exception("Ticker or FIGI required") 3217 3218 response = {} 3219 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3220 self._ticker = instrument["ticker"] 3221 self._figi = instrument["figi"] 3222 3223 if orderType == "Limit": 3224 uLogger.debug( 3225 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3226 self._ticker, self._figi, 3227 operation, lots, targetPrice, instrument["currency"], 3228 )) 3229 3230 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3231 self.body = str({ 3232 "figi": self._figi, 3233 "quantity": str(lots), 3234 "price": FloatToNano(targetPrice), 3235 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3236 "accountId": str(self.accountId), 3237 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3238 }) 3239 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3240 3241 if "orderId" in response.keys(): 3242 uLogger.info( 3243 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3244 response["orderId"], self._ticker, self._figi, operation, lots, 3245 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3246 )) 3247 3248 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3249 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3250 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3251 targetPrice, instrument["currency"], 3252 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3253 )) 3254 3255 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3256 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3257 targetPrice, instrument["currency"], 3258 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3259 )) 3260 3261 else: 3262 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3263 3264 if orderType == "Stop": 3265 uLogger.debug( 3266 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3267 self._ticker, self._figi, 3268 operation, lots, 3269 targetPrice, instrument["currency"], 3270 limitPrice, instrument["currency"], 3271 stopType, expDate, 3272 )) 3273 3274 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3275 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3276 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3277 3278 body = { 3279 "figi": self._figi, 3280 "quantity": str(lots), 3281 "price": FloatToNano(limitPrice), 3282 "stopPrice": FloatToNano(targetPrice), 3283 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3284 "accountId": str(self.accountId), 3285 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3286 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3287 } 3288 3289 if expDateUTC: 3290 body["expireDate"] = expDateUTC 3291 3292 self.body = str(body) 3293 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3294 3295 if "stopOrderId" in response.keys(): 3296 uLogger.info( 3297 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3298 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3299 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3300 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3301 TKS_STOP_ORDER_TYPES[stopOrderType], 3302 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3303 )) 3304 3305 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3306 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3307 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3308 targetPrice, instrument["currency"], 3309 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3310 )) 3311 3312 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3313 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3314 targetPrice, instrument["currency"], 3315 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3316 )) 3317 3318 else: 3319 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3320 3321 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3323 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3324 """ 3325 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3326 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3327 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3328 See also: `Order()` docstring. 3329 3330 :param lots: volume, integer count of lots >= 1. 3331 :param targetPrice: target price > 0. This is open trade price for limit order. 3332 :return: JSON with response from broker server. 3333 """ 3334 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3336 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3337 """ 3338 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3339 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3340 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3341 target price value then broker opens a limit order. See also: `Order()` docstring. 3342 3343 :param lots: volume, integer count of lots >= 1. 3344 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3345 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3346 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3347 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3348 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3349 :param expDate: string "Undefined" by default or local date in future. 3350 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3351 This date is converting to UTC format for server. 3352 :return: JSON with response from broker server. 3353 """ 3354 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3356 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3357 """ 3358 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3359 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3360 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3361 See also: `Order()` docstring. 3362 3363 :param lots: volume, integer count of lots >= 1. 3364 :param targetPrice: target price > 0. This is open trade price for limit order. 3365 :return: JSON with response from broker server. 3366 """ 3367 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3369 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3370 """ 3371 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3372 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3373 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3374 target price value then broker opens a limit order. See also: `Order()` docstring. 3375 3376 :param lots: volume, integer count of lots >= 1. 3377 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3378 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3379 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3380 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3381 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3382 :param expDate: string "Undefined" by default or local date in future. 3383 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3384 This date is converting to UTC format for server. 3385 :return: JSON with response from broker server. 3386 """ 3387 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3389 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3390 """ 3391 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3392 3393 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3394 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3395 This avoids unnecessary downloading data from the server. 3396 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3397 """ 3398 if self.accountId is None or not self.accountId: 3399 uLogger.error("Variable `accountId` must be defined for using this method!") 3400 raise Exception("Account ID required") 3401 3402 if orderIDs: 3403 if allOrdersIDs is None: 3404 rawOrders = self.RequestPendingOrders() 3405 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3406 3407 if allStopOrdersIDs is None: 3408 rawStopOrders = self.RequestStopOrders() 3409 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3410 3411 for orderID in orderIDs: 3412 idInPendingOrders = orderID in allOrdersIDs 3413 idInStopOrders = orderID in allStopOrdersIDs 3414 3415 if not (idInPendingOrders or idInStopOrders): 3416 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3417 continue 3418 3419 else: 3420 if idInPendingOrders: 3421 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3422 3423 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3424 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3425 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3426 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3427 3428 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3429 if self.moreDebug: 3430 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3431 3432 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3433 3434 else: 3435 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3436 3437 elif idInStopOrders: 3438 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3439 3440 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3441 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3442 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3443 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3444 3445 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3446 if self.moreDebug: 3447 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3448 3449 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3450 3451 else: 3452 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3453 3454 else: 3455 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3457 def CloseAllOrders(self) -> None: 3458 """ 3459 Gets a list of open pending and stop orders and cancel it all. 3460 """ 3461 rawOrders = self.RequestPendingOrders() 3462 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3463 lenOrders = len(allOrdersIDs) 3464 3465 rawStopOrders = self.RequestStopOrders() 3466 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3467 lenSOrders = len(allStopOrdersIDs) 3468 3469 if lenOrders > 0 or lenSOrders > 0: 3470 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3471 3472 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3473 3474 else: 3475 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3477 def CloseAll(self, *args) -> None: 3478 """ 3479 Close all available (not blocked) opened trades and orders. 3480 3481 Also, you can select one or more keywords case-insensitive: 3482 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3483 3484 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3485 """ 3486 overview = self.Overview(show=False) # get all open trades info 3487 3488 if len(args) == 0: 3489 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3490 self.CloseAllOrders() # close all pending and stop orders 3491 3492 for iType in TKS_INSTRUMENTS: 3493 if iType != "Currencies": 3494 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3495 3496 else: 3497 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3498 lowerArgs = [x.lower() for x in args] 3499 3500 if "orders" in lowerArgs: 3501 self.CloseAllOrders() # close all pending and stop orders 3502 3503 for iType in TKS_INSTRUMENTS: 3504 if iType.lower() in lowerArgs and iType != "Currencies": 3505 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3507 def CloseAllByTicker(self, instrument: str) -> None: 3508 """ 3509 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3510 3511 This method searches opened trade and orders of instrument throw all portfolio and then use 3512 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3513 3514 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3515 3516 :param instrument: string with ticker. 3517 """ 3518 if instrument is None or not instrument: 3519 uLogger.error("Ticker name must be defined for using this method!") 3520 raise Exception("Ticker required") 3521 3522 overview = self.Overview(show=False) # get user portfolio with all open trades info 3523 3524 self._ticker = instrument # try to set instrument as ticker 3525 self._figi = "" 3526 3527 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3528 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3529 3530 if limitAll and self.IsInLimitOrders(portfolio=overview): 3531 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3532 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3533 3534 if stopAll and self.IsInStopOrders(portfolio=overview): 3535 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3536 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3537 3538 if self.IsInPortfolio(portfolio=overview): 3539 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3540 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with ticker.
3542 def CloseAllByFIGI(self, instrument: str) -> None: 3543 """ 3544 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3545 3546 This method searches opened trade and orders of instrument throw all portfolio and then use 3547 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3548 3549 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3550 3551 :param instrument: string with FIGI id. 3552 """ 3553 if instrument is None or not instrument: 3554 uLogger.error("FIGI id must be defined for using this method!") 3555 raise Exception("FIGI required") 3556 3557 overview = self.Overview(show=False) # get user portfolio with all open trades info 3558 3559 self._ticker = "" 3560 self._figi = instrument # try to set instrument as FIGI id 3561 3562 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3563 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3564 3565 if limitAll and self.IsInLimitOrders(portfolio=overview): 3566 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3567 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3568 3569 if stopAll and self.IsInStopOrders(portfolio=overview): 3570 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3571 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3572 3573 if self.IsInPortfolio(portfolio=overview): 3574 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3575 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with FIGI id.
3577 @staticmethod 3578 def ParseOrderParameters(operation, **inputParameters): 3579 """ 3580 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3581 3582 :param operation: string "Buy" or "Sell". 3583 :param inputParameters: this is dict of strings that looks like this 3584 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3585 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3586 "prices" key: one or more prices to open limit-orders 3587 Counts of values in lots and prices lists must be equals! 3588 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3589 """ 3590 # TODO: update order grid work with api v2 3591 pass 3592 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3593 # 3594 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3595 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3596 # raise Exception("Incorrect value") 3597 # 3598 # if "l" in inputParameters.keys(): 3599 # inputParameters["lots"] = inputParameters.pop("l") 3600 # 3601 # if "p" in inputParameters.keys(): 3602 # inputParameters["prices"] = inputParameters.pop("p") 3603 # 3604 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3605 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3606 # raise Exception("Incorrect value") 3607 # 3608 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3609 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3610 # 3611 # if len(lots) != len(prices): 3612 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3613 # raise Exception("Incorrect value") 3614 # 3615 # uLogger.debug("Extracted parameters for orders:") 3616 # uLogger.debug("lots = {}".format(lots)) 3617 # uLogger.debug("prices = {}".format(prices)) 3618 # 3619 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3620 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3621 # uLogger.debug("Order parameters: {}".format(result)) 3622 # 3623 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3625 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3626 """ 3627 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3628 3629 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3630 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3631 """ 3632 result = False 3633 msg = "Instrument not defined!" 3634 3635 if portfolio is None or not portfolio: 3636 portfolio = self.Overview(show=False) 3637 3638 if self._ticker: 3639 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3640 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3641 3642 for iType in TKS_INSTRUMENTS: 3643 for instrument in portfolio["stat"][iType]: 3644 if instrument["ticker"] == self._ticker: 3645 result = True 3646 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3647 break 3648 3649 elif self._figi: 3650 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3651 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3652 3653 for iType in TKS_INSTRUMENTS: 3654 for instrument in portfolio["stat"][iType]: 3655 if instrument["figi"] == self._figi: 3656 result = True 3657 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3658 break 3659 3660 else: 3661 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3662 3663 uLogger.debug(msg) 3664 3665 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3667 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3668 """ 3669 Returns instrument from the user's portfolio if it presents there. 3670 Instrument must be defined by `ticker` (highly priority) or `figi`. 3671 3672 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3673 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3674 """ 3675 result = None 3676 msg = "Instrument not defined!" 3677 3678 if portfolio is None or not portfolio: 3679 portfolio = self.Overview(show=False) 3680 3681 if self._ticker: 3682 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3683 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3684 3685 for iType in TKS_INSTRUMENTS: 3686 for instrument in portfolio["stat"][iType]: 3687 if instrument["ticker"] == self._ticker: 3688 result = instrument 3689 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3690 break 3691 3692 elif self._figi: 3693 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3694 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3695 3696 for iType in TKS_INSTRUMENTS: 3697 for instrument in portfolio["stat"][iType]: 3698 if instrument["figi"] == self._figi: 3699 result = instrument 3700 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3701 break 3702 3703 else: 3704 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3705 3706 uLogger.debug(msg) 3707 3708 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3710 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3711 """ 3712 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3713 3714 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3715 3716 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3717 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3718 """ 3719 result = False 3720 msg = "Instrument not defined!" 3721 3722 if portfolio is None or not portfolio: 3723 portfolio = self.Overview(show=False) 3724 3725 if self._ticker: 3726 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3727 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3728 3729 for instrument in portfolio["stat"]["orders"]: 3730 if instrument["ticker"] == self._ticker: 3731 result = True 3732 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3733 break 3734 3735 elif self._figi: 3736 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3737 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3738 3739 for instrument in portfolio["stat"]["orders"]: 3740 if instrument["figi"] == self._figi: 3741 result = True 3742 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3743 break 3744 3745 else: 3746 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3747 3748 uLogger.debug(msg) 3749 3750 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif limit orders list contains some limit orders for the instrument,Falseotherwise.
3752 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3753 """ 3754 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3755 Instrument must be defined by `ticker` (highly priority) or `figi`. 3756 3757 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3758 3759 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3760 :return: list with `orderID`s of limit orders. 3761 """ 3762 result = [] 3763 msg = "Instrument not defined!" 3764 3765 if portfolio is None or not portfolio: 3766 portfolio = self.Overview(show=False) 3767 3768 if self._ticker: 3769 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3770 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3771 3772 for instrument in portfolio["stat"]["orders"]: 3773 if instrument["ticker"] == self._ticker: 3774 result.append(instrument["orderID"]) 3775 3776 if result: 3777 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3778 3779 elif self._figi: 3780 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3781 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3782 3783 for instrument in portfolio["stat"]["orders"]: 3784 if instrument["figi"] == self._figi: 3785 result.append(instrument["orderID"]) 3786 3787 if result: 3788 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3789 3790 else: 3791 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3792 3793 uLogger.debug(msg) 3794 3795 return result
Returns list with all orderIDs of opened pending limit orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of limit orders.
3797 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3798 """ 3799 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3800 3801 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3802 3803 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3804 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3805 """ 3806 result = False 3807 msg = "Instrument not defined!" 3808 3809 if portfolio is None or not portfolio: 3810 portfolio = self.Overview(show=False) 3811 3812 if self._ticker: 3813 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3814 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3815 3816 for instrument in portfolio["stat"]["stopOrders"]: 3817 if instrument["ticker"] == self._ticker: 3818 result = True 3819 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3820 break 3821 3822 elif self._figi: 3823 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3824 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3825 3826 for instrument in portfolio["stat"]["stopOrders"]: 3827 if instrument["figi"] == self._figi: 3828 result = True 3829 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3830 break 3831 3832 else: 3833 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3834 3835 uLogger.debug(msg) 3836 3837 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif stop orders list contains some stop orders for the instrument,Falseotherwise.
3839 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3840 """ 3841 Returns list with all `orderID`s of opened stop orders for the instrument. 3842 Instrument must be defined by `ticker` (highly priority) or `figi`. 3843 3844 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3845 3846 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3847 :return: list with `orderID`s of stop orders. 3848 """ 3849 result = [] 3850 msg = "Instrument not defined!" 3851 3852 if portfolio is None or not portfolio: 3853 portfolio = self.Overview(show=False) 3854 3855 if self._ticker: 3856 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3857 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3858 3859 for instrument in portfolio["stat"]["stopOrders"]: 3860 if instrument["ticker"] == self._ticker: 3861 result.append(instrument["orderID"]) 3862 3863 if result: 3864 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3865 3866 elif self._figi: 3867 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3868 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3869 3870 for instrument in portfolio["stat"]["stopOrders"]: 3871 if instrument["figi"] == self._figi: 3872 result.append(instrument["orderID"]) 3873 3874 if result: 3875 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3876 3877 else: 3878 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3879 3880 uLogger.debug(msg) 3881 3882 return result
Returns list with all orderIDs of opened stop orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of stop orders.
3884 def RequestLimits(self) -> dict: 3885 """ 3886 Method for obtaining the available funds for withdrawal for current `accountId`. 3887 3888 See also: 3889 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3890 - `OverviewLimits()` method 3891 3892 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3893 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3894 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3895 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3896 """ 3897 if self.accountId is None or not self.accountId: 3898 uLogger.error("Variable `accountId` must be defined for using this method!") 3899 raise Exception("Account ID required") 3900 3901 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3902 3903 self.body = str({"accountId": self.accountId}) 3904 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3905 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3906 3907 if self.moreDebug: 3908 uLogger.debug("Records about available funds for withdrawal successfully received") 3909 3910 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3912 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3913 """ 3914 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3915 3916 See also: `RequestLimits()`. 3917 3918 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3919 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3920 :return: dict with raw parsed data from server and some calculated statistics about it. 3921 """ 3922 if self.accountId is None or not self.accountId: 3923 uLogger.error("Variable `accountId` must be defined for using this method!") 3924 raise Exception("Account ID required") 3925 3926 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3927 3928 view = { 3929 "rawLimits": rawLimits, 3930 "limits": { # parsed data for every currency: 3931 "money": { # this is an array of portfolio currency positions 3932 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3933 }, 3934 "blocked": { # this is an array of blocked currency 3935 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3936 }, 3937 "blockedGuarantee": { # this is locked money under collateral for futures 3938 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3939 }, 3940 }, 3941 } 3942 3943 # --- Prepare text table with limits in human-readable format: 3944 if show or onlyFiles: 3945 info = [ 3946 "# Withdrawal limits\n\n", 3947 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3948 "* **Account ID:** [{}]\n".format(self.accountId), 3949 ] 3950 3951 if view["limits"]["money"]: 3952 info.extend([ 3953 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3954 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3955 ]) 3956 3957 else: 3958 info.append("\nNo withdrawal limits\n") 3959 3960 for curr in view["limits"]["money"].keys(): 3961 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3962 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3963 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3964 3965 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3966 "[{}]".format(curr), 3967 "{:.2f}".format(view["limits"]["money"][curr]), 3968 "{:.2f}".format(availableMoney), 3969 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3970 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3971 ) 3972 3973 if curr == "rub": 3974 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3975 3976 else: 3977 info.append(infoStr) 3978 3979 infoText = "".join(info) 3980 3981 if show and not onlyFiles: 3982 uLogger.info(infoText) 3983 3984 if self.withdrawalLimitsFile and (show or onlyFiles): 3985 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3986 fH.write(infoText) 3987 3988 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3989 3990 if self.useHTMLReports: 3991 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3992 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3993 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3994 3995 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3996 3997 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3999 def RequestAccounts(self) -> dict: 4000 """ 4001 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4002 4003 See also: 4004 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4005 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4006 - `OverviewUserInfo()` method 4007 4008 :return: dict with raw data from server that contains accounts info. Example of dict: 4009 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4010 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4011 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4012 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4013 """ 4014 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4015 4016 self.body = str({}) 4017 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4018 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4019 4020 if self.moreDebug: 4021 uLogger.debug("Records about available accounts successfully received") 4022 4023 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
4025 def RequestUserInfo(self) -> dict: 4026 """ 4027 Method for requesting common user's information. 4028 4029 See also: 4030 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4031 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4032 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4033 - `OverviewUserInfo()` method 4034 4035 :return: dict with raw data from server that contains user's information. Example of dict: 4036 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4037 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4038 """ 4039 uLogger.debug("Requesting common user's information. Wait, please...") 4040 4041 self.body = str({}) 4042 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4043 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4044 4045 if self.moreDebug: 4046 uLogger.debug("Records about current user successfully received") 4047 4048 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
4050 def RequestMarginStatus(self, accountId: str = None) -> dict: 4051 """ 4052 Method for requesting margin calculation for defined account ID. 4053 4054 See also: 4055 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4056 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4057 - `OverviewUserInfo()` method 4058 4059 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4060 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4061 Example of responses: 4062 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4063 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4064 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4065 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4066 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4067 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4068 """ 4069 if accountId is None or not accountId: 4070 if self.accountId is None or not self.accountId: 4071 uLogger.error("Variable `accountId` must be defined for using this method!") 4072 raise Exception("Account ID required") 4073 4074 else: 4075 accountId = self.accountId # use `self.accountId` (main ID) by default 4076 4077 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4078 4079 self.body = str({"accountId": accountId}) 4080 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4081 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4082 4083 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4084 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4085 rawMargin = {} 4086 4087 else: 4088 if self.moreDebug: 4089 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4090 4091 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
4093 def RequestTariffLimits(self) -> dict: 4094 """ 4095 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4096 4097 See also: 4098 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4099 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4100 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4101 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4102 - `OverviewUserInfo()` method 4103 4104 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4105 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4106 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4107 """ 4108 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4109 4110 self.body = str({}) 4111 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4112 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4113 4114 if self.moreDebug: 4115 uLogger.debug("Records with limits of current tariff successfully received") 4116 4117 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
4119 def RequestBondCoupons(self, iJSON: dict) -> dict: 4120 """ 4121 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4122 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4123 All dates are in UTC timezone. 4124 4125 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4126 Documentation: 4127 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4128 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4129 4130 See also: `ExtendBondsData()`. 4131 4132 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4133 If raw iJSON is not data of bond then server returns an error [400] with message: 4134 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4135 :return: dictionary with bond payment calendar. Response example 4136 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4137 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4138 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4139 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4140 """ 4141 if iJSON["figi"] is None or not iJSON["figi"]: 4142 uLogger.error("FIGI must be defined for using this method!") 4143 raise Exception("FIGI required") 4144 4145 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4146 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4147 4148 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4149 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4150 self._figi, 4151 startDate, 4152 endDate, 4153 )) 4154 4155 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4156 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4157 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4158 4159 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4160 uLogger.warning("Instrument type is not bond!") 4161 4162 else: 4163 if self.moreDebug: 4164 uLogger.debug("Records about bond payment calendar successfully received") 4165 4166 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self._ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4168 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4169 """ 4170 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4171 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4172 coupon yields, current yields and some statistics etc. 4173 4174 WARNING! This is too long operation if a lot of bonds requested from broker server. 4175 4176 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4177 4178 :param instruments: list of strings with tickers or FIGIs. 4179 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4180 for further used by data scientists or stock analytics. 4181 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4182 In XLSX-file and Pandas DataFrame fields mean: 4183 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4184 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4185 """ 4186 if instruments is None or not instruments: 4187 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4188 raise Exception("Ticker or FIGI required") 4189 4190 if isinstance(instruments, str): 4191 instruments = [instruments] 4192 4193 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4194 4195 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4196 4197 iCount = len(uniqueInstruments) 4198 tooLong = iCount >= 20 4199 if tooLong: 4200 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4201 4202 bonds = None 4203 for i, self._figi in enumerate(uniqueInstruments): 4204 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4205 4206 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4207 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4208 rawBond = self.SearchByFIGI(requestPrice=True) 4209 4210 # Widen raw data with UTC current time (iData["actualDateTime"]): 4211 actualDate = datetime.now(tzutc()) 4212 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4213 4214 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4215 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4216 4217 # Replace some values with human-readable: 4218 iData["nominalCurrency"] = iData["nominal"]["currency"] 4219 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4220 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4221 iData["aciCurrency"] = iData["aciValue"]["currency"] 4222 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4223 iData["issueSize"] = int(iData["issueSize"]) 4224 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4225 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4226 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4227 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4228 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4229 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4230 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4231 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4232 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4233 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4234 4235 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4236 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4237 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4238 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4239 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4240 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4241 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4242 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4243 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4244 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4245 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4246 4247 # Widen raw data with calendar data from `rawCalendar` values: 4248 calendarData = [] 4249 if "events" in iData["rawCalendar"].keys(): 4250 for item in iData["rawCalendar"]["events"]: 4251 calendarData.append({ 4252 "couponDate": item["couponDate"], 4253 "couponNumber": int(item["couponNumber"]), 4254 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4255 "payCurrency": item["payOneBond"]["currency"], 4256 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4257 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4258 "couponStartDate": item["couponStartDate"], 4259 "couponEndDate": item["couponEndDate"], 4260 "couponPeriod": item["couponPeriod"], 4261 }) 4262 4263 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4264 if "maturityDate" not in iData.keys(): 4265 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4266 4267 # Widen raw data with Coupon Rate. 4268 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4269 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4270 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4271 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4272 4273 # Widen raw data with Yield to Maturity (YTM) on current date. 4274 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4275 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4276 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4277 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4278 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4279 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4280 4281 iData["calendar"] = calendarData # adds calendar at the end 4282 4283 # Remove not used data: 4284 iData.pop("uid") 4285 iData.pop("positionUid") 4286 iData.pop("currentPrice") 4287 iData.pop("rawCalendar") 4288 4289 colNames = list(iData.keys()) 4290 if bonds is None: 4291 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4292 4293 else: 4294 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4295 4296 else: 4297 uLogger.warning("Instrument is not a bond!") 4298 4299 processed = round(100 * (i + 1) / iCount, 1) 4300 if tooLong and processed % 5 == 0: 4301 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4302 4303 else: 4304 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4305 4306 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4307 4308 # Saving bonds from Pandas DataFrame to XLSX sheet: 4309 if xlsx and self.bondsXLSXFile: 4310 with pd.ExcelWriter( 4311 path=self.bondsXLSXFile, 4312 date_format=TKS_DATE_FORMAT, 4313 datetime_format=TKS_DATE_TIME_FORMAT, 4314 mode="w", 4315 ) as writer: 4316 bonds.to_excel( 4317 writer, 4318 sheet_name="Extended bonds data", 4319 index=True, 4320 encoding="UTF-8", 4321 freeze_panes=(1, 1), 4322 ) # saving as XLSX-file with freeze first row and column as headers 4323 4324 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4325 4326 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4328 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4329 """ 4330 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4331 4332 WARNING! This is too long operation if a lot of bonds requested from broker server. 4333 4334 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4335 4336 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4337 extended information about bonds: main info, current prices, bond payment calendar, 4338 coupon yields, current yields and some statistics etc. 4339 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4340 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4341 for further used by data scientists or stock analytics. 4342 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4343 """ 4344 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4345 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4346 4347 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4348 4349 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4350 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4351 calendar = None 4352 for bond in extBonds.iterrows(): 4353 for item in bond[1]["calendar"]: 4354 cData = { 4355 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4356 "couponDate": item["couponDate"], 4357 "figi": bond[1]["figi"], 4358 "ticker": bond[1]["ticker"], 4359 "name": bond[1]["name"], 4360 "couponNumber": item["couponNumber"], 4361 "payOneBond": item["payOneBond"], 4362 "payCurrency": item["payCurrency"], 4363 "couponType": item["couponType"], 4364 "couponPeriod": item["couponPeriod"], 4365 "fixDate": item["fixDate"], 4366 "couponStartDate": item["couponStartDate"], 4367 "couponEndDate": item["couponEndDate"], 4368 } 4369 4370 if calendar is None: 4371 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4372 4373 else: 4374 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4375 4376 if calendar is not None: 4377 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4378 4379 # Saving calendar from Pandas DataFrame to XLSX sheet: 4380 if xlsx: 4381 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4382 4383 with pd.ExcelWriter( 4384 path=xlsxCalendarFile, 4385 date_format=TKS_DATE_FORMAT, 4386 datetime_format=TKS_DATE_TIME_FORMAT, 4387 mode="w", 4388 ) as writer: 4389 humanReadable = calendar.copy(deep=True) 4390 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4391 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4392 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4393 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4394 humanReadable.columns = colNames # human-readable column names 4395 4396 humanReadable.to_excel( 4397 writer, 4398 sheet_name="Bond payments calendar", 4399 index=False, 4400 encoding="UTF-8", 4401 freeze_panes=(1, 2), 4402 ) # saving as XLSX-file with freeze first row and column as headers 4403 4404 del humanReadable # release df in memory 4405 4406 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4407 4408 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4410 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4411 """ 4412 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4413 Also, creates Markdown file with calendar data, `calendar.md` by default. 4414 4415 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4416 4417 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4418 extended information about bonds: main info, current prices, bond payment calendar, 4419 coupon yields, current yields and some statistics etc. 4420 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4421 :param show: if `True` then also printing bonds payment calendar to the console, 4422 otherwise save to file `calendarFile` only. `False` by default. 4423 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4424 :return: multilines text in Markdown format with bonds payment calendar as a table. 4425 """ 4426 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4427 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4428 4429 infoText = "# Bond payments calendar\n\n" 4430 4431 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4432 4433 if not (calendar is None or calendar.empty): 4434 splitLine = "| | | | | | | | | |\n" 4435 4436 info = [ 4437 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4438 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4439 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4440 ] 4441 4442 newMonth = False 4443 notOneBond = calendar["figi"].nunique() > 1 4444 for i, bond in enumerate(calendar.iterrows()): 4445 if newMonth and notOneBond: 4446 info.append(splitLine) 4447 4448 info.append( 4449 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4450 " √" if bond[1]["paid"] else " —", 4451 bond[1]["couponDate"].split("T")[0], 4452 bond[1]["figi"], 4453 bond[1]["ticker"], 4454 bond[1]["couponNumber"], 4455 "{} {}".format( 4456 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4457 bond[1]["payCurrency"], 4458 ), 4459 bond[1]["couponType"], 4460 bond[1]["couponPeriod"], 4461 bond[1]["fixDate"].split("T")[0], 4462 ) 4463 ) 4464 4465 if i < len(calendar.values) - 1: 4466 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4467 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4468 newMonth = False if curDate.month == nextDate.month else True 4469 4470 else: 4471 newMonth = False 4472 4473 infoText += "".join(info) 4474 4475 if show and not onlyFiles: 4476 uLogger.info("{}".format(infoText)) 4477 4478 if self.calendarFile is not None and (show or onlyFiles): 4479 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4480 fH.write(infoText) 4481 4482 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4483 4484 if self.useHTMLReports: 4485 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4486 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4487 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4488 4489 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4490 4491 else: 4492 infoText += "No data\n" 4493 4494 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4496 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4497 """ 4498 Method for parsing and show simple table with all available user accounts. 4499 4500 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4501 4502 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4503 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4504 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4505 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4506 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4507 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4508 "closed": "—", "access": "Full access" }, ...}}` 4509 """ 4510 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4511 4512 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4513 accounts = { 4514 item["id"]: { 4515 "type": TKS_ACCOUNT_TYPES[item["type"]], 4516 "name": item["name"], 4517 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4518 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4519 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4520 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4521 } for item in rawAccounts["accounts"] 4522 } 4523 4524 # Raw and parsed data with some fields replaced in "stat" section: 4525 view = { 4526 "rawAccounts": rawAccounts, 4527 "stat": accounts, 4528 } 4529 4530 # --- Prepare simple text table with only accounts data in human-readable format: 4531 if show or onlyFiles: 4532 info = [ 4533 "# User accounts\n\n", 4534 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4535 "| Account ID | Type | Status | Name |\n", 4536 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4537 ] 4538 4539 for account in view["stat"].keys(): 4540 info.extend([ 4541 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4542 account, 4543 view["stat"][account]["type"], 4544 view["stat"][account]["status"], 4545 view["stat"][account]["name"], 4546 ) 4547 ]) 4548 4549 infoText = "".join(info) 4550 4551 if show and not onlyFiles: 4552 uLogger.info(infoText) 4553 4554 if self.userAccountsFile and (show or onlyFiles): 4555 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4556 fH.write(infoText) 4557 4558 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4559 4560 if self.useHTMLReports: 4561 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4562 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4563 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4564 4565 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4566 4567 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4569 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4570 """ 4571 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4572 4573 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4574 4575 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4576 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4577 :return: dict with raw parsed data from server and some calculated statistics about it. 4578 """ 4579 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4580 tmpTicker = self._ticker 4581 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4582 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4583 self._ticker = tmpTicker 4584 4585 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4586 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4587 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4588 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4589 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4590 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4591 4592 # This is dict with parsed common user data: 4593 userInfo = { 4594 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4595 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4596 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4597 "tariff": rawUserInfo["tariff"], 4598 } 4599 4600 # This is an array of dict with parsed margin statuses for every account IDs: 4601 margins = {} 4602 for accountId in accounts.keys(): 4603 if rawMargins[accountId]: 4604 margins[accountId] = { 4605 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4606 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4607 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4608 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4609 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4610 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4611 "missing": missing["volume"], 4612 } 4613 4614 else: 4615 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4616 4617 unary = {} # unary-connection limits 4618 for item in rawTariffLimits["unaryLimits"]: 4619 if item["limitPerMinute"] in unary.keys(): 4620 unary[item["limitPerMinute"]].extend(item["methods"]) 4621 4622 else: 4623 unary[item["limitPerMinute"]] = item["methods"] 4624 4625 stream = {} # stream-connection limits 4626 for item in rawTariffLimits["streamLimits"]: 4627 if item["limit"] in stream.keys(): 4628 stream[item["limit"]].extend(item["streams"]) 4629 4630 else: 4631 stream[item["limit"]] = item["streams"] 4632 4633 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4634 limits = { 4635 "unary": unary, 4636 "stream": stream, 4637 } 4638 4639 # Raw and parsed data as an output result: 4640 view = { 4641 "rawUserInfo": rawUserInfo, 4642 "rawAccounts": rawAccounts, 4643 "rawMargins": rawMargins, 4644 "rawTariffLimits": rawTariffLimits, 4645 "stat": { 4646 "overview": overview, 4647 "userInfo": userInfo, 4648 "accounts": accounts, 4649 "margins": margins, 4650 "limits": limits, 4651 }, 4652 } 4653 4654 # --- Prepare text table with user information in human-readable format: 4655 if show or onlyFiles: 4656 info = [ 4657 "# Full user information\n\n", 4658 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4659 "## Common information\n\n", 4660 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4661 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4662 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4663 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4664 "\n## User accounts\n\n", 4665 ] 4666 4667 for account in view["stat"]["accounts"].keys(): 4668 info.extend([ 4669 "### ID: [{}]\n\n".format(account), 4670 "| Parameters | Values |\n", 4671 "|----------------------|--------------------------------------------------------------|\n", 4672 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4673 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4674 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4675 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4676 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4677 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4678 ]) 4679 4680 if margins[account]: 4681 info.extend([ 4682 "| Margin status: | Enabled |\n", 4683 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4684 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4685 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4686 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4687 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4688 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4689 ]) 4690 4691 else: 4692 info.append("| Margin status: | Disabled |\n\n") 4693 4694 info.extend([ 4695 "\n## Current user tariff limits\n", 4696 "\n### See also\n", 4697 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4698 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4699 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4700 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4701 "\n### Unary limits\n", 4702 ]) 4703 4704 if unary: 4705 for key, values in sorted(unary.items()): 4706 info.append("\n* Max requests per minute: {}\n".format(key)) 4707 4708 for value in values: 4709 info.append(" - {}\n".format(value)) 4710 4711 else: 4712 info.append("\nNot available\n") 4713 4714 info.append("\n### Stream limits\n") 4715 4716 if stream: 4717 for key, values in sorted(stream.items()): 4718 info.append("\n* Max stream connections: {}\n".format(key)) 4719 4720 for value in values: 4721 info.append(" - {}\n".format(value)) 4722 4723 else: 4724 info.append("\nNot available\n") 4725 4726 infoText = "".join(info) 4727 4728 if show and not onlyFiles: 4729 uLogger.info(infoText) 4730 4731 if self.userInfoFile and (show or onlyFiles): 4732 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4733 fH.write(infoText) 4734 4735 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4736 4737 if self.useHTMLReports: 4738 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4739 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4740 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4741 4742 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4743 4744 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4747class Args: 4748 """ 4749 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4750 """ 4751 def __init__(self, **kwargs): 4752 self.__dict__.update(kwargs) 4753 4754 def __getattr__(self, item): 4755 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4758def ParseArgs(): 4759 """This function get and parse command line keys.""" 4760 parser = ArgumentParser() # command-line string parser 4761 4762 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4763 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4764 4765 # --- options: 4766 4767 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4768 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4769 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4770 4771 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4772 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4773 4774 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4775 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4776 4777 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4778 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4779 4780 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4781 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4782 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4783 4784 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4785 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4786 4787 # --- commands: 4788 4789 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4790 4791 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4792 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4793 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4794 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4795 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4796 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4797 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4798 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4799 4800 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4801 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4802 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4803 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4804 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4805 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4806 4807 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4808 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4809 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4810 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4811 4812 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4813 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4814 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4815 4816 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4817 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4818 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4819 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4820 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4821 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4822 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4823 4824 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4825 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4826 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4827 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4828 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4829 4830 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4831 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4832 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4833 4834 cmdArgs = parser.parse_args() 4835 return cmdArgs
This function get and parse command line keys.
4838def Main(**kwargs): 4839 """ 4840 Main function for work with TKSBrokerAPI in the console. 4841 4842 See examples: 4843 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4844 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4845 """ 4846 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4847 4848 if args.debug_level: 4849 uLogger.level = 10 # always debug level by default 4850 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4851 4852 exitCode = 0 4853 start = datetime.now(tzutc()) 4854 uLogger.debug("=-" * 50) 4855 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4856 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4857 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4858 )) 4859 4860 # trying to calculate full current version: 4861 buildVersion = __version__ 4862 try: 4863 v = version("tksbrokerapi") 4864 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4865 4866 except Exception: 4867 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4868 4869 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4870 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4871 4872 try: 4873 if args.version: 4874 print("TKSBrokerAPI {}".format(buildVersion)) 4875 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4876 4877 else: 4878 # Init class for trading with Tinkoff Broker: 4879 trader = TinkoffBrokerServer( 4880 token=args.token, 4881 accountId=args.account_id, 4882 useCache=not args.no_cache, 4883 ) 4884 4885 # --- set some options: 4886 4887 if args.more: 4888 trader.moreDebug = True 4889 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4890 4891 if args.html: 4892 trader.useHTMLReports = True 4893 4894 if args.ticker: 4895 ticker = str(args.ticker).upper() # Tickers may be upper case only 4896 4897 if ticker in trader.aliasesKeys: 4898 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4899 4900 else: 4901 trader.ticker = ticker 4902 4903 if args.figi: 4904 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4905 4906 if args.depth is not None: 4907 trader.depth = args.depth 4908 4909 # --- do one command: 4910 4911 if args.list: 4912 if args.output is not None: 4913 trader.instrumentsFile = args.output 4914 4915 trader.ShowInstrumentsInfo(show=True) 4916 4917 elif args.list_xlsx: 4918 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4919 4920 elif args.bonds_xlsx is not None: 4921 if args.output is not None: 4922 trader.bondsXLSXFile = args.output 4923 4924 if len(args.bonds_xlsx) == 0: 4925 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4926 4927 else: 4928 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4929 4930 elif args.search: 4931 if args.output is not None: 4932 trader.searchResultsFile = args.output 4933 4934 trader.SearchInstruments(pattern=args.search[0], show=True) 4935 4936 elif args.info: 4937 if not (args.ticker or args.figi): 4938 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4939 raise Exception("Ticker or FIGI required") 4940 4941 if args.output is not None: 4942 trader.infoFile = args.output 4943 4944 if args.ticker: 4945 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4946 4947 else: 4948 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4949 4950 elif args.calendar is not None: 4951 if args.output is not None: 4952 trader.calendarFile = args.output 4953 4954 if len(args.calendar) == 0: 4955 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4956 4957 else: 4958 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4959 4960 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4961 4962 elif args.price: 4963 if not (args.ticker or args.figi): 4964 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4965 raise Exception("Ticker or FIGI required") 4966 4967 trader.GetCurrentPrices(show=True) 4968 4969 elif args.prices is not None: 4970 if args.output is not None: 4971 trader.pricesFile = args.output 4972 4973 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4974 4975 elif args.overview: 4976 if args.output is not None: 4977 trader.overviewFile = args.output 4978 4979 trader.Overview(show=True, details="full") 4980 4981 elif args.overview_digest: 4982 if args.output is not None: 4983 trader.overviewDigestFile = args.output 4984 4985 trader.Overview(show=True, details="digest") 4986 4987 elif args.overview_positions: 4988 if args.output is not None: 4989 trader.overviewPositionsFile = args.output 4990 4991 trader.Overview(show=True, details="positions") 4992 4993 elif args.overview_orders: 4994 if args.output is not None: 4995 trader.overviewOrdersFile = args.output 4996 4997 trader.Overview(show=True, details="orders") 4998 4999 elif args.overview_analytics: 5000 if args.output is not None: 5001 trader.overviewAnalyticsFile = args.output 5002 5003 trader.Overview(show=True, details="analytics") 5004 5005 elif args.overview_calendar: 5006 if args.output is not None: 5007 trader.overviewAnalyticsFile = args.output 5008 5009 trader.Overview(show=True, details="calendar") 5010 5011 elif args.deals is not None: 5012 if args.output is not None: 5013 trader.reportFile = args.output 5014 5015 if 0 <= len(args.deals) < 3: 5016 trader.Deals( 5017 start=args.deals[0] if len(args.deals) >= 1 else None, 5018 end=args.deals[1] if len(args.deals) == 2 else None, 5019 show=True, # Always show deals report in console 5020 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5021 ) 5022 5023 else: 5024 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5025 raise Exception("Incorrect value") 5026 5027 elif args.history is not None: 5028 if args.output is not None: 5029 trader.historyFile = args.output 5030 5031 if 0 <= len(args.history) < 3: 5032 dataReceived = trader.History( 5033 start=args.history[0] if len(args.history) >= 1 else None, 5034 end=args.history[1] if len(args.history) == 2 else None, 5035 interval="hour" if args.interval is None or not args.interval else args.interval, 5036 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5037 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5038 show=True, # shows all downloaded candles in console 5039 ) 5040 5041 if args.render_chart is not None and dataReceived is not None: 5042 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5043 5044 trader.ShowHistoryChart( 5045 candles=dataReceived, 5046 interact=iChart, 5047 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5048 ) 5049 5050 else: 5051 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5052 raise Exception("Incorrect value") 5053 5054 elif args.load_history is not None: 5055 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5056 5057 if args.render_chart is not None and histData is not None: 5058 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5059 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5060 5061 trader.ShowHistoryChart( 5062 candles=histData, 5063 interact=iChart, 5064 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5065 ) 5066 5067 elif args.trade is not None: 5068 if 1 <= len(args.trade) <= 5: 5069 trader.Trade( 5070 operation=args.trade[0], 5071 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5072 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5073 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5074 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5075 ) 5076 5077 else: 5078 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5079 5080 elif args.buy is not None: 5081 if 0 <= len(args.buy) <= 4: 5082 trader.Buy( 5083 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5084 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5085 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5086 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5087 ) 5088 5089 else: 5090 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5091 5092 elif args.sell is not None: 5093 if 0 <= len(args.sell) <= 4: 5094 trader.Sell( 5095 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5096 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5097 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5098 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5099 ) 5100 5101 else: 5102 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5103 5104 elif args.order: 5105 if 4 <= len(args.order) <= 7: 5106 trader.Order( 5107 operation=args.order[0], 5108 orderType=args.order[1], 5109 lots=int(args.order[2]), 5110 targetPrice=float(args.order[3]), 5111 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5112 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5113 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5114 ) 5115 5116 else: 5117 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5118 5119 elif args.buy_limit: 5120 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5121 5122 elif args.sell_limit: 5123 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5124 5125 elif args.buy_stop: 5126 if 2 <= len(args.buy_stop) <= 7: 5127 trader.BuyStop( 5128 lots=int(args.buy_stop[0]), 5129 targetPrice=float(args.buy_stop[1]), 5130 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5131 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5132 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5133 ) 5134 5135 else: 5136 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5137 5138 elif args.sell_stop: 5139 if 2 <= len(args.sell_stop) <= 7: 5140 trader.SellStop( 5141 lots=int(args.sell_stop[0]), 5142 targetPrice=float(args.sell_stop[1]), 5143 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5144 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5145 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5146 ) 5147 5148 else: 5149 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5150 5151 # elif args.buy_order_grid is not None: 5152 # # update order grid work with api v2 5153 # if len(args.buy_order_grid) == 2: 5154 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5155 # 5156 # for order in orderParams: 5157 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5158 # 5159 # else: 5160 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5161 # 5162 # elif args.sell_order_grid is not None: 5163 # # update order grid work with api v2 5164 # if len(args.sell_order_grid) >= 2: 5165 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5166 # 5167 # for order in orderParams: 5168 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5169 # 5170 # else: 5171 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5172 5173 elif args.close_order is not None: 5174 trader.CloseOrders(args.close_order) # close only one order 5175 5176 elif args.close_orders is not None: 5177 trader.CloseOrders(args.close_orders) # close list of orders 5178 5179 elif args.close_trade: 5180 if not (args.ticker or args.figi): 5181 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5182 raise Exception("Ticker or FIGI required") 5183 5184 if args.ticker: 5185 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5186 5187 else: 5188 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5189 5190 elif args.close_trades is not None: 5191 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5192 5193 elif args.close_all is not None: 5194 if args.ticker: 5195 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5196 5197 elif args.figi: 5198 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5199 5200 else: 5201 trader.CloseAll(*args.close_all) 5202 5203 elif args.limits: 5204 if args.output is not None: 5205 trader.withdrawalLimitsFile = args.output 5206 5207 trader.OverviewLimits(show=True) 5208 5209 elif args.user_info: 5210 if args.output is not None: 5211 trader.userInfoFile = args.output 5212 5213 trader.OverviewUserInfo(show=True) 5214 5215 elif args.account: 5216 if args.output is not None: 5217 trader.userAccountsFile = args.output 5218 5219 trader.OverviewAccounts(show=True) 5220 5221 else: 5222 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5223 raise Exception("There is no command to execute") 5224 5225 except Exception: 5226 trace = tb.format_exc() 5227 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5228 if e in trace: 5229 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5230 break 5231 5232 uLogger.debug(trace) 5233 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5234 exitCode = 255 # an error occurred, must be open a ticket for this issue 5235 5236 finally: 5237 finish = datetime.now(tzutc()) 5238 5239 if exitCode == 0: 5240 if args.more: 5241 uLogger.debug("All operations were finished success (summary code is 0).") 5242 5243 else: 5244 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5245 os.path.abspath(uLog.defaultLogFile), exitCode, 5246 )) 5247 5248 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5249 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5250 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5251 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5252 )) 5253 uLogger.debug("=-" * 50) 5254 5255 if not kwargs: 5256 sys.exit(exitCode) 5257 5258 else: 5259 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: